Python ベースのタスクランナー Snakemake のご紹介

機械学習では多様なデータを組み合わせて使うことがおおく、様々な前処理が必要になってきます。学習・推論プロセスの再現性を高めるためには、手動でのコマンド実行や単純なシェルスクリプトのみで管理するのは相当に厳しくなってきます、、、というのは、この記事をお読みの皆さまの多くが実感されていることと思います。

そこで世の中にはタスクランナーと呼ばれるツールが開発されており、元祖である GNU make から Luigi, Airflow など様々なツールがあります。[A curated list of awesome pipeline toolkits](https://github.com/pditommaso/awesome-pipeline) には、そのようなツールのリストが作られていて、2019年12月現在、168 もの(!!)ツールが並んでいます。

そんなよくいえば百花繚乱、悪く言えば決定打に欠けるタスクランナーですが、この記事では、私が最近気に入って使っている Snakemake の魅力を伝えたいと思います。

# Snakemake の魅力?

一言にまとめるなら、 **Python のパワフルさと make のシンプルさを兼ね備えている** 点が大きな特徴です。

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141307/262191b1-0d07-b079-3a8b-b4291dee15d3.png"/>
</div>
（一番右が Snakemake のアイコンです）

## make のシンプルさ

**Snakemake は比較的シンプルな記述が可能** です。タスク記述は YAML風(?) で、わりと Makefile に近い、シンプルな書き味に仕上がっています。例えば、こんな感じです。

```Snakefile.
rule A:
    input: "httpd_access.log"
    output: "analyzed_log.txt"
    shell: "analysis_command {input} > {output}"
```

けっこう Makefile に似ています、よね！？ 

同じく Python ベースのタスクランナーである Luigi, AirFlow といったタスクランナーでは、各タスクをクラスとして定義します。入力などをメソッドとして記述するため、少々記述が煩雑になります。

Snakemake のセットアップは Python 環境があれば `conda install snakemake`,  `pip install snakemake` といったコマンド1つで終わりで、こちらもシンプルです。AirFlow のようにサーバを立てたりといった面倒はありません。make のように「最初からシステムに入っている」にはさすがに及びませんが。:sweat:

ちなみに、上の Snakefile があるフォルダで snakemake を実行すると、 `analysis_command httpd_access.log > analyzed_log.txt`が実行されます。`analysis_command` を `cat` にした場合の実行例は次のようになります。（`-p` は実行するコマンドを表示するオプション）

```
$ snakemake -p
Building DAG of jobs...
Using shell: /bin/bash
Provided cores: 1 (use --cores to define parallelism)
Rules claiming more threads will be scaled down.
Job counts:
        count   jobs
        1       A
        1

[Tue Nov 17 21:55:22 2020]
rule A:
    input: httpd_access.log
    output: analyzed_log.txt
    jobid: 0

cat httpd_access.log > analyzed_log.txt
[Tue Nov 17 21:55:22 2020]
Finished job 0.
1 of 1 steps (100%) done
Complete log: /Users/smatsumoto/tmp/snakemake/.snakemake/log/2020-11-17T215522.025457.snakemake.log
```

## Python のパワフルさ

Snakemake は Python ベースのツールで、タスク記述も Python スクリプトに変換して実行しています。つまり、 

　　**いざとなったら Python コードでやりたい放題** 

です。このあたりは最近よくある Python ベースのタスクランナーに共通する利点です。

さきほどの記述にあった `shell:` のところを `script:` とすると、Python コードを書くこともできます。またルール記述以外の部分は通常の Python コードとして実行されるので、適当な関数を定義して、ルールのなかでそれらの関数を使うこともできます。当然、Python と同じく `#` でコメントを書くこともできます。

Python ベースで動作するおかげでいろいろな処理を自然に書くことができます。make では `$@` とか `$^` とか、いろいろなナゾの記号を使うことで複雑な処理を実現していましたが、Snakemake では

　**謎の記号を使う必要がありません!!**

ちゃんと名前がついた変数になっていることは、個人的にとてもありがたいです。ネットでの検索のしやすさが段違いなので。

# 特徴的な機能

色々と特徴的な機能がありますが、ここでは下記 2 点を取り上げます。

- ワイルドカード
- リモートファイルのサポート

## ワイルドカード

冒頭に挙げた例は `httpd_access.log` を入力して `analyzed_log.txt` を出力するという単純極まる例でしたが、多くのタスクランナーと同じく Snakemake にも一つのタスク記述で複数ファイルの変換を指定できる書き方があります。それが「ワイルドカード」です。

```Snakefile.
rule A:
    input: "img_{img_num}.jpg"
    output: "annot_{img_num}.txt"
    shell: "analysis_command {input} > {output}"
```

この記述の `{img_num}` の部分がワイルドカードです。中の文字列は任意のものを指定可能です。このように書いておくと、`annot_001.txt`, `annot_002.txt`,... の生成には、それぞれ `img_001.jpg`, `img_002.jpg`,... が必要、ということが指定できます。

また、ワイルドカードは複数書いても構いません。例えば `{date}/img_{img_num}.jpg` といったような書き方も OK です。

make のパターンルールに相当する機能ですが、謎の記号みがなくていい感じかなと思うところです。

## リモートファイルのサポート

機械学習でもクラウドのストレージを使うことも多くなってきてると思います。Snakemake はそのようなリモートファイルのサポートもあります。

書き方は非常に単純で `input: S3.remote('input.txt')` などと書くと、処理の開始前にダウンロードしてくれます。（S3 は、AWS のクラウドストレージサービス）

また、`output: S3.remote('output.txt')` とすると、処理成功時にアップロードしてくれます。前述のワイルドカードもサポートされているので、ローカルにファイルがある場合とあまり変わらずにタスクを書くことができます。

そのため、AWS の各種コンピューティングサービス（EC2, ECS, Batch など)で稼働する解析処理も簡単に書くことができます。 

# どんな用途に向いている？

**小規模チームや初期実験・探索フェーズでの利用** にフィットするかと思います。

セットアップは非常に容易ですし、学習コストも高くありません。記述がシンプルなので、色々と試行錯誤もやりやすいと思います。開発者の方はバイオインフォマティクスの研究者の方なこともあってか、研究寄りの状況で使いやすいツールになっているように感じます。

また **make を使っているけど、変数が覚えられない** という方や、 **もう少し柔軟にタスクを書きたい** という方にはピッタリだと思います。これは私自身がそのパターンですね :smiley_cat: Python と親和性の高い、今ドキの使い方に合わせやすい make という感覚で使えると思います。

一方、タスク実行状況の可視化などは強くないので、大規模な ML チームの開発基盤としては若干、力不足かもしれません。可視化ツールも出てきているようですが、Airflow や Luigi と比較できるほど成熟してないかな、という印象です。

<!--
# 使ってみるには、、、

## インストール

Anaconda/conda 環境をお使いの方は `conda install snakemake`, 通常の Python 環境をお使いの場合は `pip install snakemake` でインストール可能です。

## お試し実行

適当なフォルダを作って、一番上の例のテキストを `Snakefile` として保存します。

```Snakefile
gen_B:
	input: fileA
	output: fileB
	shell: commandA2B < {input} > {output}

gen_C:
	input: fileB
	output: fileC
	shell: commandB2C < {input} > {output}
```

そして `snakemake` コマンドを実行すると、下記のような結果がコンソールに出力され、`fileC` が生成されているはずです。

```
```

-->

# 実際のつかいこなし

私自身は Snakemake と make の両方を使っています。

make で済むところは make で済ませ、解析フローの本体のように `$@` とか変数を使いたくなってくるところには Snakemake を使うことが多いです。だいたいの場合、

- make は 「1 ファイルで複数のスクリプトがおいておける置き場」として利用
- Snakemake で主要な分析処理を実行

という役割分担ですね。

```Makefile.
# Makefile - さまざまなコマンドの寄せ集め場所として。
analyze:
    snakemake analyze  # 分析の中身は Snakemake で管理

clean:
    rm -rf work/*
```

# Snakemake の改善希望な点

Snakemake のタスク記述はけっこうシンプルですが、make と比べるとちょっと煩雑です。基本的に Snakemake ではファイル間の依存関係を記述する前提になっているため、タスク同士をつなぐ依存関係を書きたいときは `output: touch(some-file)` 等と、タスク完了を示す空ファイルを touch する形になります。make とくらべるとちょっと冗長ですね。

あと同じ記述の繰り返しが増えがちです。このあたりは、タスクがちゃんとした Python クラスになってる Luigi や AirFlow だと、似て非なるタスクをループでバンバン生成したりと、よりやりたい放題できるので、YAML 風記述を採用したことのデメリットですね[^1]。ワイルドカードで色々と対応できるっちゃできるのですが、ファイル名に制約があったり、ワイルドカードでどう書くといいか知恵を絞るという少々不毛な作業になることもあったりなかったり。

・・・このあたりを改善できるようなコードをこの年末年始に書きたい、なぁ。

# まとめ

以上、Snakemake の紹介でした。凝ったツールに手を出すのは気が重いけど、make を使い続けるのもしんどいなぁ、と思ってるような方はぜひ一度、 Snakemake を試してもらえればと思います。使ってみての疑問点など、コメントに書いてもらえば分かる範囲でお答えできればと思います。

なお、Snakefile の書き方やコマンドラインオプションなどをまとめた [Snakemake チートシート](https://qiita.com/smatsumt/items/998c2e8101e0b57f8060)という記事も書いているので、興味のある方は参照してもらえればうれしいです。


[^1]: Snakemake は、タスク定義を Python オブジェクトに内部で置き換えてから、実行します。逆に言えばタスク定義の時点では Python オブジェクトではないため、自由に生成したりはできない、ということですね。

<!--
このあたり、改善するプルリクを作って送れれば、と思い、その前提となる改善するプルリクを準備中だったりします。
（-> デコレータでごまかせないか画策中。。。）

チュートリアルがちょっとアレ

生成するファイルベースの書き方、パラメータをファイル名に入れ込むやり方とは微妙に相性が良くない。
-->

<!--これらのツールは一連の処理をまとめて実行するためのツールで、2回目以降は指定された依存関係をもとに **必要なタスクだけを** 再実行することが大きな特徴です。-->

<!--
例えば `入力画像(input.jpg)から解析を実施し(result.json) -> fileC` という一連の処理をさせたい場合、下記のような設定ファイルを用意します。

```Snakefile
gen_B:
	input: input.jpg
	output: result.json
	shell: detect_object.py input.jpg > result.json

gen_C:
	input: result.json
	output: result.jpg
	shell: draw_result.py input.jpg result.json -o output.jpg
```

`snakemake` コマンドを実行すると、`detect_object.py`, `draw_result.py` がそれぞれ実行され、`output.jpg` が生成されます。

そして次にもう一度 `snakemake` コマンドを実行すると、今度は `output.jpg` が生成済みのためタスクが実行されません。

また、手動で `result.json` を編集してから `snakemake` コマンドを実行すると、snakemake がファイルの更新時刻から `draw_result.py` のみを再実行してくれます。

また、Snakemake の設定ファイルでは「ワイルドカード」が使えます。これを使うと、例えば

`input/*.jpg -> analysis_result/*.json -> output/*.jpg`

といったように、`input` フォルダにある一連のファイル群に対して同じ処理を実行したい場合にも

```Snakefile
gen_B:
	input: input/{fn}.jpg
	output: analysis_result/{fn}.json
	shell: commandA2B < {input} > {output}

gen_C:
	input: img: input/{fn}.jpg, json: analysis_result/{fn}.json
	output: output/{fn}.jpg
	shell: commandB2C < {input} > {output}
```

という設定ファイルを用意することで、最終結果を `output` フォルダに出力させることができます。

大量の画像ファイルを粛々と、、、というパターンが多い
パラメータを色々と変えながら
-->