メタコマンド実行スクリプトを Python で書いてみた

半指導している学生が Python のライブラリを使って実験しているので,最低限の知識はあった方が良いかと思って,今週半ばぐらいから Python でプログラムを書いたりしている.以下は,Ruby で書いたスクリプトの中で,最も使用頻度の高いメタコマンド実行スクリプトPython 翻訳.

#!/usr/bin/env python
#  cont: execte meta-commands with disjunctive arguments
import sys, re, os

if len (sys.argv) < 3:
    sys.exit ("Usage: run [n][c|p] command")

# handle options
opt = sys.argv[1]
nruns, flag = (int (opt[:-1]), opt[-1] == 'p') \
    if re.match (r'\d+[cp]$', opt) else sys.exit ("unrecognized opt: %s" % opt)

def pair (args):
    """Return a list of command by pairing disjunctive arguments."""
    xs = set (len (x) for x in args)
    return (zip (*(x * max (xs) if len (x) == 1 else x for x in args))) \
        if len (xs) <= 2 else sys.exit ("unbalanced pairing: %s" % args)

def combo (args):
    """Return a list of command by combinating disjunctive arguments."""
    return [[]] if not args \
        else ([x] + rest for x in args[0] for rest in combo (args[1:]))

args = []
for x in sys.argv[2:]:
    m = re.search (r'\[(.+?)\]', x) # disjunctive arguments
    args.append ([x.replace (m.group (), s) for s in m.group (1).split ('|')] \
                     if m else [x])
else: # execute
    for com in (' '.join (z) for z in (pair (args) if flag else combo (args))):
        if nruns == 1:
            sys.stderr.write ("> %s\x1b[0m\n" % com)
        for j in range (nruns):
            if nruns != 1:
                sys.stderr.write ("\x1b[34m(%d/%d)> %s\x1b[0m\n"
                                  % (j + 1, nruns, com))
            for line in os.popen (com):
                sys.stderr.write (line)
            sys.stderr.write ("\n")

機械学習のハイパーパラメタなど,引数の一部だけ変えて複数のコマンドを連続実行するときに使う.例えば

> cont 2p run svm-train -t 1 -d '[1|2]' -c '0.[1|005]' train 'model[1|2]'
  • > run svm-train -t 1 -d 1 -c 0.1 train model1
run svm-train -t 1 -d 1 -c 0.1 train model1 run svm-train -t 1 -d 2 -c 0.005 train model2 run svm-train -t 1 -d 2 -c 0.005 train model2 > cont 1c run svm-train -t 1 -d '[1|2]' -c '0.[1|005]' train model
  • > run svm-train -t 1 -d 1 -c 0.1 train model
run svm-train -t 1 -d 1 -c 0.005 train model run svm-train -t 1 -d 2 -c 0.1 train model run svm-train -t 1 -d 2 -c 0.005 train model

みたいな感じ.消費メモリ・時間を計測する run コマンドを組み込んで,平均消費メモリ・時間を出すように改造しても良いかも.Ruby スクリプトを愚直に Python に書き換えたので,Python の作法として不自然なところもあるかも知れない.
まだほんのさわり程度の文法知識しかないものの,Python はスライス,内包表現/ジェネレータを積極的に使えば Ruby より短いコードで済ませることもできそうな感じ.処理速度も ruby 1.9.2 v.s. python 2.6 だと Python の方が数倍程度速かった.Python (programming language) - Wikipedia によれば,

While offering choice in coding methodology, the Python philosophy rejects exuberant syntax, such as in Perl, in favor of a sparser, less-cluttered grammar.

とのことだが,思っていたよりは書き方に多様性があるように感じる(リスト内包表現,ジェネレータ,map/filter,またメソッドの代わりにスライスなど; 効率に差が出る場合もあるようだ).','.join() がなぜキモイのか - methaneのブログ とか,len が関数になっている理由 - methaneのブログ など,違和感を感じる場面もあるけど,慣れればそれほど気にならないかな.正規表現がやや使いにくいけど,普段スクリプトを書くには python でも良さそうだ.
二値素性向けの Passive Aggressive-I も簡単に書ける.svm-light フォーマットで学習/テストする場合は以下のようになる(素性値は全て 1 とされていると見なして無視).

#!/usr/bin/env python
import sys, collections

# handle arguments
if len (sys.argv) != 5:
    sys.exit ("Usage: %s train test c iter\n" % sys.argv[0])

train, test  = sys.argv[1:3]
c    = float (sys.argv[3])
iter = int   (sys.argv[4])

# read data into memory; assuming value :1
examples = [(int (line[:2]), [int (fi[:-2]) for fi in line[3:-1].split (' ')])
            for line in open (train)]

# estimate w
w = collections.defaultdict (float)
for i in range (iter):
    for y, x in examples:
        margin = y * sum (w[fi] for fi in x)
        if margin <= 1:
            t = y * min (c, (1 - margin) / len (x))
            for fi in x:
                w[fi] += t
    sys.stderr.write(".")
sys.stderr.write("done.\n")

# test
result = [0] * 4
for line in open (test):
    m = sum (w[int (fi[:-2])] for fi in line[3:-1].split (' '))
    result[((int (line[:2]) > 0) << 1) + (m > 0)] += 1

nn, np, pn, pp = result # there was a bug, here (corrected)
sys.stderr.write ("acc. %2.3f%% (pp %d) (pn %d) (np %d) (nn %d)\n" % \
                      (float (pp + nn) * 100 / (sum (result)), pp, pn, np, nn))

素性を文字列のまま扱ったほうが(int (fi[:-2]) -> fi[:-2])実行速度は速いのだけど,メモリを二倍近く消費する.ほぼ同じプログラムを C++ で書いたら97行と Python 実装の約3倍のコード量.スクリプト言語なので,Python 実装の実行速度が C++ 実装の3050倍遅いのはやむなしというところなんだろうけど,メモリ消費が5倍以上だったのは全く納得いかない.
ちなみに,おまけ - ny23の日記 の単語カウントスクリプトPython 翻訳

#!/usr/bin/env python
import collections
counter = collections.defaultdict (int)

for line in open ("unigram_raw.txt"):
    counter[line[:-1]] += 1

for k, v in counter.items ():
    print k, v

は 125s ほどで終了したので,Rubyワンライナーの10倍以上 (1.8.6 比,1.9.2 比だと約3倍) 速く,C++ の4倍遅い程度で済んだ.メモリは C++ 実装 (ハッシュ)の3倍消費した.単語カウントに関しては,Perlワンライナー (5.12.3) は Python と大体同じ速さで,メモリ消費は C++ より少し多い程度.