diff --git a/document/source/figures/c_vs_accuracy.png b/document/source/figures/c_vs_accuracy.png new file mode 100644 index 0000000..9471cc4 Binary files /dev/null and b/document/source/figures/c_vs_accuracy.png differ diff --git a/document/source/figures/size_vs_accuracy.png b/document/source/figures/size_vs_accuracy.png new file mode 100644 index 0000000..cd8c5a6 Binary files /dev/null and b/document/source/figures/size_vs_accuracy.png differ diff --git a/document/source/index.rst b/document/source/index.rst index 2928b47..0522a23 100644 --- a/document/source/index.rst +++ b/document/source/index.rst @@ -12,6 +12,7 @@ https://github.com/pfi/maf で開発されています。 .. toctree:: :maxdepth: 2 + quickstart tutorial/index 古いドキュメント diff --git a/document/source/quickstart.rst b/document/source/quickstart.rst new file mode 100644 index 0000000..7296f33 --- /dev/null +++ b/document/source/quickstart.rst @@ -0,0 +1,433 @@ +クイックスタート +================= + +maf は機械学習の実験管理などをやりやすくするためのツールです。ここでは maf の基本的な使い方を具体例を通して紹介します。 + +インストール +------------ + +maf では実験を一つのディレクトリの中で行うことを想定しています。 +まずはどこかのディレクトリに移動しましょう。 + +.. code-block:: sh + + $ mkdir experiment && cd experiment + +mafを動かすためには、 ``waf`` および ``maf.py`` という二つのpythonスクリプトが必要です。 +これらは実験を行う各ディレクトリ毎に配置するのが良いでしょう。 +以下のコマンドでダウンロードできます。 + +.. code-block:: sh + + $ wget https://github.com/pfi/maf/raw/master/waf && wget https://github.com/pfi/maf/raw/master/maf.py && chmod +x waf + +mafでは実験の手順を ``wscript`` というファイルに記述していきます。 +以下は実験ではないですが、 ``wscript`` の書き方の簡単な例です。 + +.. code-block:: sh + + $ cat wscript + import maf + + def configure(conf): pass + + def build(exp): + exp(target='output', + rule='echo hoge > ${TGT}') + + $ ./waf configure # wscriptに書いたconfigureが実行される。実験を始める前に必要。 + Setting top to : /.../experiment + Setting out to : /.../experiment/build + 'configure' finished successfully (0.004s) + $ ./waf build # wscriptに書いたbuildの手順が実行される。 + Waf: Entering directory `/.../experiment/build' + [1/1] output: -> build/output + Waf: Leaving directory `/.../experiment/build' + 'build' finished successfully (0.016s) + $ cat build/output + hoge + +``wscript`` 自体もpythonのスクリプトとなっていて、これが ``waf`` を実行することで読み込まれます。 +最初のコマンド ``configure`` はとりあえず気にする必要はありませんが、実験を行う前に実行する必要があります。 +次の ``build`` がメインの実行で、 ``wscript`` に書いた ``build`` 関数を実行します。 +この例では、ここに書いた ``rule`` 内の ``${TGT}`` が自動で置き換えられた、以下のコマンドが実行されます。 + +.. code-block:: sh + + $ echo hoge > build/output + +この ``${TGT}`` の対応が、 ``target`` 変数で指定されています。 +mafでは実験途中のモデルや結果を繋げて処理を行っていきますが、このように結果は全て ``build`` ディレクトリ以下に作られます。 + +``./waf build`` がメインのコマンドですが、これは頻繁に使うので、 ``build`` を省略しても良いことになっています。 + +.. code-block:: sh + + $ ./waf # ./waf build の省略形 + +以下の例では、このように ``build`` を省略します。 + +実際の実験例 +------------ + +もう少し具体的な例を通して、mafでの実験手順を紹介します。 +mafが特に役に立つのは、様々なパラメータや手法に関する試行錯誤を行わないといけない場面です。 +簡単な例として、ここでは `LIBLINEAR `_ を使用し、このパラメータのチューニングを行う場面を考えます。 +mafを使えば、Figure :num:`c-vs-accuracy` のような特定のパラメータを変化させた場合の性能の変化や、Figure :num:`size-vs-accuracy` のような訓練データ量に対する性能の変化といった結果を、20行程度の ``wscript`` を書くことで得ることができます。 +またmafでは試行錯誤をやりやすくするために、パラメータの設定の変更などが容易に行えます。 +例えばFigure :num:`c-vs-accuracy` ではパラメータ ``C`` の値を ``10^-3`` から ``1`` まで変化させていますが、もっと大きくした ``C=10`` の値まで含めたいと思った場合、そのように ``wscript`` を書き換えれば ``C=10`` での実験のみが追加で行われ、新しいグラフを得ることができます。 +つまり既に実行済みのパラメータ設定を自動的に検出し、図を描くのに足りない設定のみを追加で実行してくれます。 + +.. _c_vs_accuracy: +.. figure:: figures/c_vs_accuracy.png + :height: 350px + :scale: 80% + + 手法毎の、パラメータCを変化させたときの性能変化 + +.. _size_vs_accuracy: +.. figure:: figures/size_vs_accuracy.png + :height: 350px + :scale: 80% + + 訓練データを増やしたときの学習曲線 + +データとツールの準備 +~~~~~~~~~~~~~~~~~~~~ + +ここでは実験用のツールとして `LIBLINEAR `_ を、データとして `20 News `_ という文書分類タスク用のデータを用います。 +LIBLINEARは各データが特徴ベクトルに変換された入力を必要としますが、MNIST をこの形式に変換したデータが手に入るのでこれを使いましょう。 + +.. code-block:: sh + + $ wget http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/news20.scale.bz2 + $ wget http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/news20.t.scale.bz2 + $ bunzip2 *.bz2 + $ ls + maf.py news20.scale news20.t.scale waf wscript + +``news20.scale`` が訓練データ、 ``news20.t.scale`` がテストデータです。 + +LIBLINEARがマシンにインストールされていない場合は事前にインストールが必要です。 +以下は各種パッケージ管理でのインストール例です。 + +.. code-block:: sh + + $ sudo apt-get install liblinear1 liblinear-tools # apt + $ sudo yum install liblinear liblinear-devel # yum + $ brew install liblinear # homebrew + +これで実験の準備が整いました。 + +図を描くためのwscript +~~~~~~~~~~~~~~~~~~~~~ + +wscript を以下のように書き換えて実行 ( ``./waf`` ) すると、Figure :num:`c-vs-accuracy` を得ることができます。 +結果は ``build/accuracy.png`` に得られます。 + +.. code-block:: python + + import maf + import maflib.util + import maflib.plot + + def configure(conf): pass + + def build(exp): + exp(source='news20.scale', # exp(...) を複数定義すると、それらが順に実行される + target='model', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1]}), + rule='liblinear-train -s ${s} -c ${C} ${SRC} ${TGT} > /dev/null') + + exp(source='news20.t.scale model', + target='accuracy', + rule='liblinear-predict ${SRC} /dev/null > ${TGT}') + + exp(source='accuracy', + target='accuracy.json', + rule=maflib.rules.convert_libsvm_accuracy) + + exp(source='accuracy.json', + target='accuracy.png', # 最終的な結果が、 build/accuracy.png に得られる + for_each='', + rule=maflib.plot.plot_line( + x = {'key': 'C', 'scale': 'log'}, + y = 'accuracy', + legend = {'key': 's'})) + +この例では、 ``build(exp)`` の中に計4個の ``exp(...)`` が存在します。 +実験は、訓練データからのモデルの学習、それを使ってのテストデータの予測など複数の手順からなりますが、この際の各手順を ``exp(...)`` の中に定義しています。 +具体的には、このwscriptでは以下のような手順で実験が進みます。 + +1. 様々なパラメータの設定で ``liblinear-train`` を実行し、訓練後のモデルファイルの集合を得る。 +2. 得られた各モデルファイルを用いて ``liblinear-predict`` を実行し、モデル毎のテストデータに対する精度を計算する。 +3. 得られた各精度の数値をjson形式に変換する。次のグラフ描画のために必要。 +4. これまで得られた各モデル毎の精度をもとに、グラフを描画する。 + +パラメータの管理 +~~~~~~~~~~~~~~~~~ + +Figure :num:`c-vs-accuracy` では、横軸で ``C`` を変化させ、各 ``s`` の値毎に精度をプロットしています。 +この ``C`` と ``s`` はどちらもLIBLINEARのパラメータです。 + +.. code-block:: none + + $ liblinear-train + Usage: train [options] training_set_file [model_file] + options: + -s type : set type of solver (default 1) + for multi-class classification + 0 -- L2-regularized logistic regression (primal) + 1 -- L2-regularized L2-loss support vector classification (dual) + 2 -- L2-regularized L2-loss support vector classification (primal) + 3 -- L2-regularized L1-loss support vector classification (dual) + 4 -- support vector classification by Crammer and Singer + ... + -c cost : set the parameter C (default 1) + ... + +このような図を描くためには、各 ``s`` の値毎に、 ``C`` の値を変えて訓練及びテストを行った結果を保持しないといけません。 +mafではこのようなパラメータの組み合わせを保持することが簡単にできます。 +最初の ``exp`` 呼び出しは以下のようになっています。 + +.. code-block:: python + + exp(source='news20.scale', + target='model', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1]}), + rule='liblinear-train -s ${s} -c ${C} ${SRC} ${TGT} > /dev/null') + +一番最初の例と違うのは、 ``parameters`` という変数が指定されている点です。 +ここで用いている :py:func:`maflib.util.product` は、このように複数のリストを与えると、それらの直積を計算してくれる関数です。 +そして、この組み合わせ全てに対して、 ``rule`` で指定したコマンドが実行されます。 +この ``exp`` では、以下のように計 ``4*4=16`` 個のコマンドが実行されます。 + +.. code-block:: sh + + $ liblinear-train -s 0 -c 0.001 news20.scale build/model/0-model > /dev/null + $ liblinear-train -s 1 -c 0.001 news20.scale build/model/1-model > /dev/null + $ liblinear-train -s 2 -c 0.001 news20.scale build/model/2-model > /dev/null + ... + $ liblinear-train -s 2 -c 1 news20.scale build/model/14-model > /dev/null + $ liblinear-train -s 3 -c 1 news20.scale build/model/15-model > /dev/null + +このように全てのパラメータの組に対して訓練が実行され、各実行で ``${s}`` などの部分が代入されています。 +また ``${TGT}`` の代入のされ方は先ほどと似ていますが、 ``build/model/0-model`` のように、 ``target`` で指定した出力先はディレクトリとなり、その中にパラメータ別の結果がまとめられます。 + +実験同士の依存関係 +~~~~~~~~~~~~~~~~~~~ + +以下は二番目の ``exp`` 呼び出しです。 + +.. code-block:: python + + exp(source='news20.t.scale model', + target='accuracy', + rule='liblinear-predict ${SRC} /dev/null > ${TGT}') + +この意味を理解するのも、ここから実際にどのようなコマンドが生成されるかを見たほうが分かりやすいと思います。 +これは以下のように、先ほどと同じく16個のコマンドを生成します。 + +.. code-block:: sh + + $ liblinear-predict news20.t.scale build/model/0-model /dev/null > build/accuracy/0-accuracy + $ liblinear-predict news20.t.scale build/model/1-model /dev/null > build/accuracy/1-accuracy + ... + $ liblinear-predict news20.t.scale build/model/15-model /dev/null > build/accuracy/16-accuracy + +これを見ると以下のことが分かります。 + +1. ``${SRC}`` には、指定した ``source`` が展開された値が代入されます。 + ``source`` には ``'news20.t.scale model'`` のように複数の値を指定することができます。 + このうち ``news20.t.scale`` は現在のディレクトリのファイルを指し、全ての実行で変わりませんが、 ``model`` は例のように、先ほど作られた ``build/model/`` 以下のファイルが順に指定され、実行されます。 +2. ``${TGT}`` は、前回と似たように展開されます。 + 今回は ``parameters`` を指定していませんが、代わりに ``model`` が一つ一つのパラメータの組み合わせと結びついているので、各 ``model`` 毎に、 ``build/accuracy`` 以下に結果が格納されます。 + +ここで重要な点は、実験同士の依存関係です。 +今回 ``source`` に指定した ``model`` は、先ほど ``target`` に指定した ``model`` と同じオブジェクトを指す、という風に理解されます。 +このように、 ``source`` や ``target`` に直接ファイルが存在しない名前を指定すると、それら二つの実験の間に依存関係を成り立たせることができます。 +mafはこの依存関係を自動的に解決し、例えば + +.. code-block:: sh + + $ liblinear-predict news20.t.scale build/model/0-model /dev/null > build/accuracy/0-accuracy + +というコマンドは、 + +.. code-block:: sh + + $ liblinear-train -s 0 -c 0.001 news20.scale build/model/0-model > /dev/null + +が終了し ``0-model`` が生成されるまで実行されません。 + +関数ルール +~~~~~~~~~~~ + +三番目の ``exp`` 呼び出しはデータの変換を行います。 + +.. code-block:: python + + exp(source='accuracy', + target='accuracy.json', + rule=maflib.rules.convert_libsvm_accuracy) + +この ``accuracy`` は二番目の ``target`` と同じものを指すので、これらの間には依存関係が生まれます。 + +ここでは ``rule`` の指定方法が先ほどまでと異なっています。 +これまでの例では、 ``rule`` にはシェルのコマンドを指定してきましたが、より柔軟にpythonのコマンドを指定することもできます。 +これはその例となっていて、 :py:func:`maflib.rules.convert_libsvm_accuracy` 関数を実行します。 +自分で関数ルールを定義する方法は、...をご覧ください。 + +ここでは何が起きているかだけの説明にとどめます。 + +.. code-block:: sh + + $ cat build/accuracy/0-accuracy + Accuracy = 88.99% (8899/10000) + $ cat build/accuracy.json/0-accuracy.json + {"accuracy": 88.99} + + +``0-accuracy`` などは、 LIBSVM の標準出力を保持したものです。 +:py:func:`maflib.rules.convert_libsvm_accuracy` は、この出力形式を読み取り、それをjsonに変換します。 +このようにjsonにするのは、次のプロットがjson形式の入力を必要とするためです。 +LIBLINEARの出力をjsonに変換するには、この用意された関数を使えば良いのですが、他のソフトの出力をjsonに変換するには、似たような関数を定義する必要があります。 + +集約とプロット +~~~~~~~~~~~~~~~ + +これまでで ``build/accuracy.json`` が得られていて、この中にはパラメータの組み合わせ毎のテストデータに対する精度がjson形式で保存されています。 +最後に、この結果をもとに、パラメータ毎の精度をグラフにまとめます。 + +.. code-block:: python + + exp(source='accuracy.json', + target='accuracy.png', + for_each='', + rule=maflib.plot.plot_line( + x = {'key': 'C', 'scale': 'log'}, + y = 'accuracy', + legend = {'key': 's'})) + +ここで見慣れないのは ``for_each`` です。 +今回は空文字を指定していますが、これは今回の ``plot`` など、得られた結果を集約する場合に必要になるものです。 +今回は全ての結果を一つのグラフにまとめていますが、場合によっては、特定のパラメータの値毎にグラフを複数に分けたい場合も存在します。 +そのような時は、 ``for_each`` に指定したパラメータの値毎にグラフが作成されます。 + +:py:func:`maflib.plot.plot_line` も、用意された、グラフを書く際に便利な関数です。 +例のように、各x,y軸の設定などを指定することができます。 +``y = 'accuracy',`` は、 accuracy.jsonの ``{"accuracy": 88.99}`` の値を取り出していることを意味します。 + +``for_each`` を使うタスクは一般に集約タスクと呼ばれます。 +これについての詳細は、 ... をご覧ください。 + +実験設定の追加 +~~~~~~~~~~~~~~ + +これまではmafを、パラメータの組み合わせを変化させた場合のグラフの簡単描画ツールのように説明してきました。 +そういう側面もあるのですが、mafのもう一つの売りは、実験結果を構造的に管理することで、結果の再利用がしやすくなる点です。 +言い換えると、実験の条件を書き換えたり修正した場合、まだ実行されていない、実行する必要のあるタスクだけを自動的に実行します。 + +例えば Figure :num:`c-vs-accuracy` で、大まかに ``C`` を大きくするほど精度が良くなる傾向が見られるので、より大きな ``C=10`` での実験も追加してグラフを書きたいとします。 +この場合、次のように最初 ``exp`` の ``C`` に値を追加しましょう。 + +.. code-block:: python + + 'C': [0.001, 0.01, 0.1, 1, 10]}), + +その後再実行を行うと、 ``C=10`` まで含んだグラフを得ることができます。 + +.. code-block:: python + + $ ./waf + Waf: Entering directory `/Users/noji/private-maf/experiment/build' + [20/61] 16-model: news20.scale -> build/model/16-model + [21/61] 17-model: news20.scale -> build/model/17-model + ... + [61/61] accuracy.png: build/accuracy.json/4-accuracy.json build/accuracy.json/10-accuracy.json ... + +この際に、訓練や評価などは、 ``C=10`` の設定が関わる部分だけが追加で実行されます。 +最後のプロットは全ての結果をまとめるので、新しく得られた結果があればそれを関知し、更新します。 + +これとは逆に、 ``C`` の値を減らした場合、例えば + +.. code-block:: python + + 'C': [0.001, 0.01, 0.1]}), + +とすると、すでに必要な結果は全て揃っていますが、グラフを描く際の範囲が変化したことを関知し、 + +.. code-block:: python + + $ ./waf + Waf: Entering directory `/Users/noji/private-maf/experiment/build' + [37/37] accuracy.png: build/accuracy.json/4-accuracy.json build/accuracy.json/10-accuracy.json ... + +と、グラフ描画を新しく行います。 +新しいグラフには、 ``C`` が ``0.1`` 以下の範囲で結果が描画されます。 + +このように ``wscript`` を書き換えて再実行する場合、その都度バージョン管理で結果を保持しておくことをお勧めします。 +TODO: もうちょっと書く。 + +別の実験:データ量を変化させる +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +これまで一つのwscriptを例に、mafの簡単な使い方を紹介しましたが、何ができるかをもう少し見るために、別の実験を紹介します。 +この例では、Figure :num:`size-vs-accuracy` のグラフを描くことを考えます。 +先ほどはパラメータ ``C`` の値を変化させましたが、今回は訓練データを変化させた場合のデータ量に対する精度をプロットしています。 +異なる実験は異なるディレクトリで行った方が良いので、まずディレクトリを移動して、ファイルを用意します。 + +.. code-block:: sh + + $ mkdir ../experiment2 && cd ../experiment2 + $ cp ../experiment/maf.py ./ + $ cp ../experiment/waf ./ + +wscriptは以下のようになります。 + +.. code-block:: python + + import maf + import maflib.util + import maflib.plot + + def configure(conf): pass + + def build(exp): + exp(source='news20.scale', + target='traindata', + parameters=maflib.util.product({'datasize': [1000, 3000, 6000, 9000, 12000, 15000]}), + rule='head -n ${datasize} ${SRC} > ${TGT}') + + exp(source='traindata', + target='model', + parameters=maflib.util.product({'s': [0, 1, 2, 3]}), + rule='liblinear-train -s ${s} -c 1.0 ${SRC} ${TGT} > /dev/null') + + exp(source='news20.t.scale model', + target='accuracy', + rule='liblinear-predict ${SRC} /dev/null > ${TGT}') + + exp(source='accuracy', + target='accuracy.json', + rule=maflib.rules.convert_libsvm_accuracy) + + exp(source='accuracy.json', + target='size_vs_accuracy.png', + for_each='', + rule=maflib.plot.plot_line( + x='datasize', + y='accuracy', + legend={'key': 's'})) + +基本的に先ほどとかなり似ています。 +異なるのは、主に最初の二つです。 +まず異なるサイズの訓練データ ``traindata`` を準備します。 +``news20.scale`` は一行が一つの訓練例となっているので、これでOKです。 +二つ目の ``exp`` が先ほどの最初に対応しますが、今回は ``traindata`` を指定しているので、用意した異なる長さのデータ毎に、各パラメータで実行が行われます。 +このようにパラメータの設定は追加していくことが可能で、今回の例では ``model`` や ``accuracy`` などは、各 ``datasize`` と ``s`` の組み合わせ毎に結果が保持されます。 + diff --git a/document/source/tutorial/index.rst b/document/source/tutorial/index.rst index 0e90988..6aa7e4d 100644 --- a/document/source/tutorial/index.rst +++ b/document/source/tutorial/index.rst @@ -10,3 +10,5 @@ maf の使い方をひと通り学びたい人は、このチュートリアル maf_basic parameter parameter_combination + own_rule + own_aggregator diff --git a/document/source/tutorial/maf_basic.rst b/document/source/tutorial/maf_basic.rst index 7ea6ec9..46e04aa 100644 --- a/document/source/tutorial/maf_basic.rst +++ b/document/source/tutorial/maf_basic.rst @@ -8,6 +8,8 @@ この章では、maf をつかった簡単なファイル処理を通して、maf の基本的な概念を学びます。 はじめなので、基本的な用語の説明などですこし分量が多いですが、内容はとても簡単ですのでさらさらとお読みください。 +.. _first-wscript: + 実験内容 -------- @@ -200,6 +202,8 @@ wscript 内で ``message`` と書いていても、実体は ``build/message`` 多くの場合、出力ノードの名前をそのままタスクの名前として用います。 たとえば上の例だと ``message`` タスク、 ``message2`` タスク、のように呼びます。 +.. _directory-node: + ディレクトリノード ------------------ diff --git a/document/source/tutorial/own_aggregator.rst b/document/source/tutorial/own_aggregator.rst new file mode 100644 index 0000000..03f741a --- /dev/null +++ b/document/source/tutorial/own_aggregator.rst @@ -0,0 +1,125 @@ +集約処理を自分で書く +==================== + +.. + 対象読者:関数ルールの書き方を分かっている人 + 目標:集約処理の関数を定義できるようになる + +前章ではルールをPythonの関数として定義する方法を紹介しましたが、集約を行う処理を自分で定義する方法については説明していませんでした。 +ここでは自分で集約処理を行うルールを書く方法を見ていきましょう。 + +集約タスクの例 +---------------- + +集約タスクを定義すること自体は、ほとんど通常のタスクと同じように行うことができます。 +最初はまたあまり意味のない例から始めましょう。 + +.. code-block:: python + + @maflib.util.rule + def combine(task): + for node in task.inputs: # task.inputs は集約された入力ノードのリスト + task.outputs[0].write(node.read(), 'a') + + def build(exp): + exp(target='number', + parameters=maflib.util.product({'n': [0, 1, 2]}), + rule='echo n=${n} > ${TGT}') + + exp(source='number', + target='combined', + for_each='', # 集約タスクであることを知らせるために for_each が必要 + rule=combine) + +この wscript を実行すると、以下の結果が得られます。 + +.. code-block:: sh + + ./waf + Waf: Entering directory ... + [1/4] 0-number: -> build/number/0-number + [2/4] 2-number: -> build/number/2-number + [3/4] 1-number: -> build/number/1-number + [4/4] combined: build/number/2-number build/number/1-number build/number/0-number -> build/combined + $ cat build/combined + n=2 + n=1 + n=0 + +最初のルールにより、 ``n`` の値が異なる ``number`` メタノードが作られます。 +そして、この各ノードが、次のルールにより集約されています。 +二番目のルールには ``for_each=''`` がありますが、これを書くことにより、このルールが集約タスクのルールであると判断されます。 +これは代わりに ``aggregate_by='n'`` としても同じです。 + +集約タスクのタスクオブジェクト ``task`` は、通常のタスクオブジェクトと似たように扱うことができますが、いくつか注意点があります。 + +(1) ``task.inputs`` は ``source`` で指定したメタノードを集約した後の、ノードのリストを返します。 + この例では、 ``n`` の値の異なる三つのノードからなるリストを返します。 +(2) ``task.parameter`` は ``for_each`` で指定したパラメータの現在の値を辞書形式で返します。 + この場合は空の辞書です。 +(3) ``task.source_parameters`` は集約タスク固有のフィールドで、 ``task.inputs`` と組になっています。 + 現在 ``task.inputs == [2-number, 1-number, 0-number]`` となっていますが、このとき ``task.source_parameters == [{'n': 2}, {'n': 1}, {'n': 0}]`` となります。 + つまり、各ノードが対応するパラメータを、リスト形式で保持します。 + +これらのフィールドを扱うことにより、原理的には、複数のパラメータの組み合わせを束ねる集約タスクを自由に定義することができます。 +集約タスクに関するもう一つの注意は、入力ノードは一つのみである、という点です。 +これは、 ``task.inputs`` の各要素が、同じメタノードの異なるパラメータのノードである、ということからくる制限です。 + +@maflib.util.jason_aggregator +-------------------------------- + +maf では、上のように生で集約タスクを書く代わりに、特定の集約タスクを書きやすくするための仕組みが存在します。 +以下では、その中の代表的な二つである二つのデコレータである、 ``json_aggregator`` と ``aggregator`` を紹介します。 + +``json_aggregator`` は、 :py:func:`maflib.rules.min` や :py:func:`maflib.rules.max` など、 maf に用意された多くのルールで使われています。 +これは、複数の json の結果を集約して、一つの json にまとめる際に便利なデコレータです。 + +何が便利になるのかを見るために、まずは ``max()`` と動作と、それを上で紹介した生の集約タスクとして定義するとどうなるか、を説明します。 + +以下はクイックスタートで用いた ``wscript`` を少し修正したものです。 + +.. code-block:: python + + import maf + import maflib.util + import maflib.plot + + def configure(conf): pass + + def build(exp): + exp(source='news20.scale', + target='model', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1], + 'B': [-1, 1]}), # バイアス項を追加 + rule='liblinear-train -s ${s} -c ${C} -B ${B} ${SRC} ${TGT} > /dev/null') + + exp(source='news20.t.scale model', + target='accuracy', + rule='liblinear-predict ${SRC} /dev/null > ${TGT}') + + exp(source='accuracy', + target='accuracy.json', + rule=maflib.rules.convert_libsvm_accuracy) + + exp(source='accuracy.json', + target='max_accuracy.json', + aggregate_by='B', + rule=maflib.rules.max(key='accuracy')) + + exp(source='max_accuracy.json', + target='accuracy.png', + for_each='', + rule=maflib.plot.plot_line( + x = {'key': 'C', 'scale': 'log'}, + y = 'accuracy', + legend = {'key': 's'})) + + +@maflib.util.aggregator +-------------------------- + + + + diff --git a/document/source/tutorial/own_rule.rst b/document/source/tutorial/own_rule.rst new file mode 100644 index 0000000..b7ca198 --- /dev/null +++ b/document/source/tutorial/own_rule.rst @@ -0,0 +1,315 @@ +ルールをPythonスクリプトで定義する +================================== + +.. + 対象読者:パラメータを使ったタスクとコマンドルールの書き方がわかっている人 + 目標:独自のルールを定義できるようになる + +これまで、各タスクのルールにはシェルスクリプトを記述していました。 +本章では、このルールをPythonの関数で定義する方法を学びます。 +これにより、シェルスクリプトで簡単に書けないような処理を各タスクで行うことができ、実験の幅が大きく広がります。 + +単純な例 +--------- + +まず感覚を掴んでもらうため、シェルスクリプトでもできることを関数で書くとどのようになるか、を説明します。 + +以下は一番最初の :ref:`first-wscript` で紹介したwscriptの一部です。 + +.. code-block:: python + + import maf + + def configure(conf): + pass + + def build(exp): + exp(target='message', rule='echo "Hello" > ${TGT}') + +これと同じことを、関数ルールを用いて書こうとすると、以下のようになります。 + +.. code-block:: python + + import maf + import maflib.util # 1 + + @maflib.util.rule # 2 + def my_echo(task): + task.outputs[0].write('Hello') + + def configure(conf): + pass + + def build(exp): + exp(target='message', rule=my_echo) # 3 + +変更した点をまとめると、 + +(1) 関数を定義するためのライブラリの読み込み +(2) 関数の定義 +(3) ``rule`` に、定義した関数を指定 + +となります。ルールを定義する場合、上のように、 + +.. code-block:: python + + @maflib.util.rule + def function_name(task): + # 実際の処理を記述 + +という部分は、こうするという決まりです。こうやって定義しておくと、 + +.. code-block:: python + + exp(target='...', rule=function_name) + +などと、exp呼び出しの際に ``rule`` に関数名を指定することで、この関数が実行されるようになります。 + +タスクオブジェクト +------------------ + +定義した関数の中身を詳しく見ていきます。 + +.. code-block:: python + + @maflib.util.rule + def my_echo(task): + task.outputs[0].write('Hello') + +``my_echo`` が実行される際、この実行は、引数である ``task`` というオブジェクトを通して行われます。 +このオブジェクトはタスクオブジェクトと呼ばれます。 +これは、そのタスクに固有の情報を全て保持しています。 +例えば、上の ``task.outputs`` はそのタスクの出力ノードの情報を保持します。 +後で紹介するように、タスクに紐づいたパラメータにアクセスしたい場合、 ``task.parameter`` でアクセスができます。 + +``task.outputs[0]`` は、出力ノードを指します。 +今回の例では ``exp(target='message', ...)`` と記述したように出力のメタノードは一種類ですが、 ``task.output`` 自体はリストなので、最初の要素を指定するために ``[0]`` と指定しなければいけません。 +ここではノードオブジェクトの ``write()`` 関数を使っています。 +これは、引数で与えた文字列をノードに書き出す関数で、つまり、先ほどの ``echo`` と全く同じことをしていることになります。 + +最初の ``@maflib.util.rule`` デコレータについては説明を省略します。 +これは独自のルールを定義する際に必要なものだと覚えておいてください。 + +実際の使用例 +-------------- + +先ほどの例はほぼ実用性はありませんが、関数ルールが具体的にどのような場面で役に立つかを紹介します。 +基本的にこれは、シェルで一行で書くことができない処理、もしくはできるけれど、pythonで書いた方が簡単な処理を行いたい場合に利用するものです。 + +クイックスタートで用いた例を再掲します。 + +.. code-block:: python + + import maf + import maflib.util + import maflib.plot + + def configure(conf): pass + + def build(exp): + exp(source='news20.scale', + target='model', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1]}), + rule='liblinear-train -s ${s} -c ${C} ${SRC} ${TGT} > /dev/null') + + exp(source='news20.t.scale model', + target='accuracy', + rule='liblinear-predict ${SRC} /dev/null > ${TGT}') + + exp(source='accuracy', + target='accuracy.json', + rule=maflib.rules.convert_libsvm_accuracy) + + exp(source='accuracy.json', + target='accuracy.png', + for_each='', + rule=maflib.plot.plot_line( + x = {'key': 'C', 'scale': 'log'}, + y = 'accuracy', + legend = {'key': 's'})) + +ここで用いている :py:func:`maflib.rules.convert_libsvm_accuracy` が、関数ルールの使用例です。 +二つ目のexp呼び出しまでで得られた ``accuracy`` メタノードの各ノードは、次のように、LIBSVM の標準出力を保持しています。 + +.. code-block:: sh + + $ ./cat build/accuracy/0-accuracy + Accuracy = 88.99% (8899/10000) + +``maflib.rules.convert_libsvm_accuracy`` は、これらをjsonに変換します。 + +.. code-block:: sh + + $ ./cat build/accuracy.json/0-accuracy.json + {"accuracy": 88.99} + +これは次のように実装されています。 + +.. code-block:: python + + @maflib.util.rule + def convert_libsvm_accuracy(task): + content = task.inputs[0].read() # ノードの read() メソッドで中身をstringで得る + j = {'accuracy': float(content.split(' ')[2][:-1])} # 数値の部分を取り出してdictionaryに変換 + task.outputs[0].write(json.dumps(j)) # json.dumps() でjsonに変換し、書き出す + +こういった文字列処理などをシェルのコマンドで実現するのは大変なので、関数ルールが便利です。 + +パラメータとsubprocess +------------------------ + +別の例として、関数の中でこれまでのようなコマンド呼び出しを行う例を紹介します。 + +上のLIBLINEARを例にとって、例えば、設定毎に訓練にかかった時間を計測したいとします。 +この場合は、最初のexp呼び出しを次のように書き換えればOKです。 + +.. code-block:: python + + @maflib.util.rule + def train_with_time(task): + import time, subprocess + begin = time.time() # 開始時刻 + + cmd = 'liblinear-train -s {s} -c {C} {src} {tgt} > /dev/null'.format( + s = task.parameter['s'], # タスクのパラメータはこのように辞書形式でアクセスできる + C = task.parameter['C'], + src = task.inputs[0].abspath(), + tgt = task.outputs[0].abspath()) + + subprocess.check_call(cmd, shell = True) # 関数内でのコマンド呼び出しにはsubprocessを使う + + sec = time.time() - begin # 終了時刻から時間を計測 + + task.outputs[1].write(str(sec)) # それを二番目の出力ノードに書き出す + + def build(exp): + exp(source='news20.scale', + target='model train_time', # train_time という出力ノードを追加 + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1]}), + rule=train_with_time) # コマンドの代わりに関数を呼び出す + + ... + +ここでは ``target`` を二種類に増やしています。 +またこれまで ``rule='liblinear_train ...'`` とコマンドを記述していた部分の処理が、関数 ``train_with_time`` に置き換わっています。 + +ここでは、シェルのコマンドの実行時間を、関数内で計測しているので、関数内部でコマンドを呼び出さないといけません。 +このような場合は、 python の `subprocess `_ モジュールなどを使って、自分でコマンドを呼び出さないといけません。 +ここで使っている ``subprocess.check_call()`` は、引数のコマンドを実行します。 +その際 ``shell = True`` を与えておかないと、このようにコマンドを一つの文字列で指定することができないので、注意して下さい。 + +タスクパラメータ +----------------- + +最後に、タスクに固有の定数を外部から与える方法を紹介します。 + +ここではまた前に戻って、簡単なechoを関数で置き換える例を取り上げます。 +以下の二つのタスク定義で、異なるのは ``Hello`` か ``Hi`` かだけなので、これを抽象化したいとしましょう。 + +.. code-block:: python + + def build(exp): + exp(target='message', rule='echo "Hello" > ${TGT}') + exp(target='message2', rule='echo "Hi" > ${TGT}') + +この際、同じことを次のように書くことができます。 + +.. code-block:: python + + @maflib.util.rule + def my_echo(task): + task.outputs[0].write(task.parameter['msg']) + + def build(exp): + exp(target='message', rule=my_echo(msg='Hello')) + exp(target='message', rule=my_echo(msg='Hi')) + +このように、 ``rule`` の関数指定に、引数でキーと値を指定すると、それらが関数内で ``parameter`` として使えるようになります。 + +これを使うと、例えば関数内で使われる定数を外部から与えることが可能になります。 + +.. code-block:: python + + @maflib.util.rule + def train_with_time(task): + ... + + cmd = 'liblinear-train -s {s} -c {C} -B {B} {src} {tgt} > /dev/null'.format( + s = task.parameter['s'], + C = task.parameter['C'], + B = task.parameter['B'], # 'B' の値を追加。これは定数で与える。 + src = task.inputs[0].abspath(), + tgt = task.outputs[0].abspath()) + + subprocess.check_call(cmd, shell = True) + ... + + def build(exp): + exp(source='news20.scale', + target='model train_time', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1]}), + rule=train_with_time(B = 0)) + + ... + +ここで ``B`` の値は関数の中で変化しない定数です。 +このようなタスク固有の定数を与える際には、この機能が役に立ちます。 + +以下は少し補足的な内容です。このような方法ではなく、 ``parameters`` の中に一種類の値を指定してはダメなのかと思われるかもしれませんが、これには別の問題が発生します。 + +.. code-block:: python + + # 以下のコードは非推薦 + def build(exp): + exp(source='news20.scale', + target='model train_time', + parameters=maflib.util.product({ + 's': [0, 1, 2, 3], + 'C': [0.001, 0.01, 0.1, 1], + 'B': [0]}), # Bは一種類だけなので、定数の役割を果たす。 + rule=train_with_time) + + ... + +この方法の問題点として、 ``B`` というパラメータが以降のタスクで使えなくなってしまいます。 +:ref:`meta-node-to-parameterized-task` で述べたように、複数のタスクで同じ種類のパラメータを定義した場合、それらに食い違いが発生すると、タスクが実行されません。 +このように発生する問題を防ぐために、mafの書き方として、先に述べたように **タスクに定数を指定する場合は、呼び出し時に指定する** ことを推薦しています。 + +maflib.rules +--------------- + +:py:mod:`maflib.rules` モジュールには、いくつかの便利な関数が実装済みなので、参考にして下さい。 +例えば :py:func:`maflib.rules.download` は、指定したURLからファイルをダウンロードして使えるようにします。 + +.. code-block:: python + + def build(exp): + exp(target='news20.scale', + rule=maflib.rules.download( + url='http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/news20.scale.bz2', + decompresss_as='bz2')) # 省略した場合、解凍を行わない + +このように、 ``url`` でダウンロード先のURLを、 ``decompress_as`` で解凍方法を指定できます。 +この後のタスクでは、 ``source='news20.scale'`` とすると、解凍したファイルを入力ノードに指定することができるようになります。 + +まとめ +-------- + +本章では maf の機能のうち、以下の項目を紹介しました。 + +- 関数ルールの定義の仕方と呼び出し方 +- 関数内でのタスクオブジェクトの使い方 + + - ``inputs, outputs, parameters`` の呼び出し + - subprocessを使ったコマンド実行 + +- タスクパラメータによる定数の指定法 + +ルールを関数で定義することで、コマンドでは表せないような複雑な処理を実験の中に組み込むことができます。 +本章では、簡単なタスクを自分で定義する方法を紹介しましたが、次章では、集約タスクを自分で定義する方法を扱います。 diff --git a/document/source/tutorial/parameter_combination.rst b/document/source/tutorial/parameter_combination.rst index 08cd8bc..c2bb597 100644 --- a/document/source/tutorial/parameter_combination.rst +++ b/document/source/tutorial/parameter_combination.rst @@ -137,6 +137,8 @@ maf の精神としては、あくまで同じメタノードには同じ処理 同じターゲットへのタスクを 2 つ以上書くのは、ほかに手段がないときに限りましょう。 (この例のように、入力ファイルをパラメータで区別したい場合に必要となることが多いです) +.. _meta-node-to-parameterized-task: + パラメータづけられたタスクにメタノードを入力する ------------------------------------------------