自然言語処理的な何かを始めようとした時に試行錯誤が辛かったのでそれなりに楽にできる環境を考えた

はじめに

久しぶりに幾つかのステップに分かれる変換が行われた結果を見ながら試行錯誤するというような作業をした。手軽に再実行などができる環境がないと辛くなってきたので、それなりに楽にできる環境を整えようとしてみた。

データの変換は幾つかのステップに分かれていたが、各変換の結果が把握しやすいように各ステップごとにファイルとして残しておくことにした。これは結果的に言えば良かった。

どのあたりが辛かったか

まずいちいち手動で実行したいスクリプトを指定して変換するのが面倒くさいというのがあった。なのでMakefileを書いてまとめて実行できるようにした。

ところが、作ったMakefileのタスクを実行した際に全てのファイルを対象に実行されるように作ってしまったため、パラメータを調整する度に全ての再実行が行われて待ち時間が増えてしまった。ある特定のデータに対してのみ一連の変換を動かすというようなことがしたくなった。

また、変換するスクリプトが思わぬデータを返すときに、入力となるファイルを直接指定して、ある特定の1ステップの変換の結果を見てみたいということもあった。

まとめると以下の様なことがしたくなった。

  • 一連のステップを一回で全ての入力に対して実行されて欲しい
  • 一連のステップを段階的に実行したい
  • 特定のデータに対してのみ一連のステップを実行したい
  • 特定のステップを特定のデータに対してのみ実行して結果を確認してみたい
  • (変換をまとめて実行するスタイルと個別に実行するスタイルを透過的に使い分けたい)
  • (ファイル出力せずに実行結果が標準出力に表示されて欲しい場合もある)

行なったこと

一連のステップを一回で全ての入力に対して実行されて欲しい

これはMakefileを作成した

一連のステップを段階的に実行したい

Makefileをone,two,three,..と安直な名前ではあるがタスクに分割した

特定のデータに対してのみ一連のステップを実行したい

後述する TARGET ?= '' によるhack的なものを使った

特定のステップを特定のデータに対してのみ実行して結果を確認してみたい

どこからでもコマンドを呼べるようにした。

詳細

実際に行ったことを箇条書きにすると以下の様な感じになる。

  • 呼ばれる処理自体はimportできるようにしておく(ひとつ前の記事)
  • misc/cli/以下にコマンドとして実行できるようなモジュールを書く
  • handofcatsを使って関数をコマンドに変換する
  • 入力として渡されるものがファイルであってもディレクトリであっても透過的に実行できるような仕組みを作る
  • 出力先が指定されていない場合には標準出力に出力する

各ステップ毎の変換コマンドは以下の様な感じで作る。

(misc.cli.summary)

# -*- coding:utf-8 -*-
import json
import os.path
import sys
from handofcats import as_command
from misc.utils import (
    FileOrPortOpener,
    iterate_file_or_directory,
    get_dirpath_and_filepath
)
from misc import get_summary


@as_command
def main(src, outfile=None):
    dirpath, filepath = get_dirpath_and_filepath(src)
    open_file_or_stdout = FileOrPortOpener(outfile)

    source = filepath or dirpath
    for path in iterate_file_or_directory(source, glob_arg="*.json"):
        with open(path) as rf:
            nums = json.load(rf)

        name = os.path.basename(path)
        result = get_summary(name, nums)
        with open_file_or_stdout(path, "w") as wf:
            wf.write(json.dumps(result, ensure_ascii=False, indent=2))
        sys.stderr.write(".")

handofcats.as_command で関数定義を解析してコマンドを作っている。(コマンドの作成自体はclickなど他のものを使っても良い)

使い方

misc packageを環境にインストールしておくことにしたので python -m misc.cli.<command name> で各ステップごとの変換処理をどこからでも呼び出せるようになった。

また、ファイル、ディレクトリ、標準出力の取り扱いが便利な感じになった。これは、 misc.util で定義した get_dirpath_and_filepath, iterate_file_or_directory, FileOrPortOpener の組み合わせによるものが大きい。(実装は このような感じ)

上で定義されたような main() 関数は以下のようなわりと便利な挙動を示すようになる。

# argparseで定義したのと同様のもの
$ python -m misc.cli.summary
usage: summary.py [-h] [--outfile OUTFILE] src
summary.py: error: the following arguments are required: src

# 特定のステップを特定のファイルに対してのみ実行したい
$ python -m misc.cli.summary normalized/001.json
{
  "src": "001.json",
  "sum": 5.312648568992502,
  "mean": 0.5312648568992503,
  "median": 0.573514029907352,
  "sd": 0.2982669362736558
}.

# --outfileでファイルに出力できる
$ python -m misc.cli.summary normalized/00json --outfile=/tmp/a.json
.$ cat /tmp/a.json
{
  "src": "001.json",
  "sum": 5.312648568992502,
  "mean": 0.5312648568992503,
  "median": 0.573514029907352,
  "sd": 0.2982669362736558
}

# 入力にディレクトリを渡すことでディレクトリ内のファイルをまとめて実行できる。
$ python -m misc.cli.summary normalized    {
  "src": "001.json",
  "sum": 5.312648568992502,
  "mean": 0.5312648568992503,
  "median": 0.573514029907352,
  "sd": 0.2982669362736558
}{
  "src": "002.json",
  "sum": 3.86964224290879,
  "mean": 0.38696422429087896,
  "median": 0.2614330324240291,
  "sd": 0.32058722945508217
}{
  "src": "003.json",
  "sum": 6.124455469688946,
  "mean": 0.6124455469688946,
  "median": 0.7594443275687857,
  "sd": 0.34935500078411313
}{
  "src": "004.json",
  "sum": 4.692322862949306,
  "mean": 0.46923228629493063,
  "median": 0.49596603944787065,
  "sd": 0.2599569259515991
}{
  "src": "005.json",
  "sum": 6.989107727734095,
  "mean": 0.6989107727734095,
  "median": 0.7770891862230952,
  "sd": 0.212051542313431
}{
  "src": "006.json",
  "sum": 4.501856990402756,
  "mean": 0.45018569904027556,
  "median": 0.5215996315833538,
  "sd": 0.29008885725202976
}{
  "src": "007.json",
  "sum": 3.5159586267734997,
  "mean": 0.35159586267735,
  "median": 0.28864790323731593,
  "sd": 0.2682584931875431
}{
  "src": "008.json",
  "sum": 2.8953625550871314,
  "mean": 0.28953625550871315,
  "median": 0.1904136703261906,
  "sd": 0.2436802013464383
}{
  "src": "009.json",
  "sum": 4.376789352146964,
  "mean": 0.43767893521469636,
  "median": 0.31580471834912965,
  "sd": 0.2938839312041112
}{
  "src": "010.json",
  "sum": 5.2573943944951855,
  "mean": 0.5257394394495185,
  "median": 0.5016153456727482,
  "sd": 0.2837614668907492
}.
# outfileにもディレクトリを渡すことでまとめて変換できる
$ python -m misc.cli.summary normalized/ --outfile=summary/
..........

おまけ

Makefileには ?= という代入演算子が存在する。これは既に値が設定されていたらスキップする代入のようなもの。

pythonで言うと or を使った代入と同様

value = value or "default"

これを以下のようにしてTARGETという変数に対して使うことで、1ファイルのみを指定するかディレクトリ全体を指定するか使い分けられるようになる。

# ディレクトリ全体をまとめて実行
$ make default
# 1ファイルのみを対象に実行
$ TARGET="001.json" make default
# TARGETが指定されていない場合空文字となりディレクトリとして扱われるようになる
TARGET ?= ""

three:
  mkdir -p summary
  python -m misc.cli.summary --outfile=summary/${TARGET} normalized/${TARGET

結果

捗った。

試してみたい人(主に未来の自分)のために同様の体験が得られるサンプルプロジェクトを作ってみた。 https://github.com/podhmo/data-conversion-sample