Papermillでノートブックをバッチ実行して分散環境におけるハイパーパラメータ探索を効率化する方法

# 1. はじめに

こんにちは，(株) 日立製作所  [Lumada Data Science Lab.](https://www.hitachi.co.jp/New/cnews/month/2020/03/0330d.html) の露木です。

今回はJupyterノートブックで引数付きのバッチ実行を可能にし，ハイパーパラメータ探索などを効率化する[Papermill](https://papermill.readthedocs.io/en/latest/usage-workflow.html) というOSSを紹介します。Papermillの設計思想や他ツールと組み合わせたシステムの全体像については，開発元の[Netflixが詳細な記事](https://medium.com/netflix-techblog/notebook-innovation-591ee3221233) を公開しているのでそちらもぜひご覧ください。

## 1.1. Jupyterの利点

データサイエンス分野の方々であれば，機械学習スクリプトの試作やレポート作成のために[Jupyterノートブック](https://jupyter.org/)の形式でコーディングをすることはよくあるかと思います。Jupyterノートブックであれば，図1のようにMarkdownの形式の説明文とともにソースコードと実行結果をまとめられるため，第三者に計算の内容と結果をわかりやすく伝えられます (いわゆる[文芸的プログラミング](https://ja.wikipedia.org/wiki/%E6%96%87%E8%8A%B8%E7%9A%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)を自然に実践できます)。


> ![jupyter1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/4fd5e533-4ed2-0b2e-7c3f-ccb751b75cf9.png)
> **図1. Jupyterノートブックによるコーディング例**


## 1.2. 問題提起: ノートブックの引数にパラメータを与えられない

ところで，一般にモデルの学習処理には時間がかかります。その上，ハイパーパラメータを変えながらモデルの汎化性能が最も高くなる条件を探索するとなれば，ハイパーパラメータの組み合わせの数に，テストデータと訓練データの分割を変えた交差検証の回数を掛け算した回数だけ学習処理を実行しなければなりません。例えば，SVMの2種類のハイパーパラメータ $\gamma$ と $C$ についてそれぞれ6種類ずつ組み合わせをとり，さらに交差検証を5回実施するのであれば， $6 \times 6 \times 5 = 180$ 回の学習処理を実行することになります。

このようなハイパーパラメータ探索のための学習処理は (ベイズ最適化のようなテクニックを使わなければ) 基本的に独立していますから，並列化によって効率化しやすい処理です。先程の180回の学習処理であっても，引数が異なるバッチジョブとして[ジョブスケジューラ](https://qiita.com/MasafumiTsuyuki/items/acf4b19b54937496e32b)に登録し，180台の計算ノードで並列処理すれば1回分の学習処理と同じ時間で終了します。

しかし，Jupyterノートブックは人間がインタラクティブに操作することを前提にしているため，このようなバッチ実行をしにくい問題があります。細かい話をすれば，Jupyterでもコマンドラインで `jupyter nbconvert --to notebook --execute --inplace hoge.ipynb`  とすればバッチ実行できます。しかし，一般のPythonスクリプトのように引数を与えられません。つまり，先程のデータサイエンスのユースケースで言えば，ハイパーパラメータやデータセットのファイル名などが異なった多数のノートブックを作成しなければなりません (図2参照)。

> ![jupyter_problem.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/13b05b0f-fdbd-1e2f-46ad-73a7dff974da.png)
> **図2. Jupyterノートブックのバッチ実行における課題。** Jupyterだけだとパラメータをノートブックに埋め込まなければならないので，バッチ実行前に多数作成しなければならない。

## 1.3. 対策: Papermillをつかう

この問題に対処するために，Netflixが開発したOSSの[Papermill](https://papermill.readthedocs.io/en/latest/usage-workflow.html) を利用できます。Papermillを使えば，ノートブックに引数を与えた上でバッチ実行できる (図3参照) ため，パラメータごとにノートブックを作成せずに済みます。そこで，本稿ではPapermillを使ったノートブックの実行方法を説明します。


> ![papermill.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/e361e63c-33ce-cf00-2e2e-91d632ecf1e7.png)
> **図3. Papermillの利点1。** パラメータをバッチ実行時の引数として指定できるので，ノートブックを多数作成する必要がない


# 2. Papermillの使い方
## 2.1. 環境準備

最初にノートブックを実行したいマシン (お手元のノートPCや計算ノードなど) にPapermillをインストールします。インストールは以下のコマンド実行で可能です。

```bash:インストールのためのコマンド
pip3 install 'papermill[all]'
```

## 2.2. ノートブックの準備

まず，バッチ実行するためのノートブックを準備します。今回は単にPapermillの動作確認をするだけですので，設定したパラメータを表示するだけの図4のようなノートブックを作ります。

> ![input.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/bfb5a943-3b5e-e906-c34a-3f27c4145d66.png)
> **図4. バッチ実行するノートブックの例**

次に，Papermillに対応するためにこのノートブックを修正します。といっても修正するのは変数を指定する部分のみです。

具体的には，ノートブックのどこか1つのセルで引数としたい変数をまとめて定義するように修正します。図4の例であれば，1つのセル内で以下のように `alpha` や `ratio` といった変数を指定するように修正します。なお，Papermill実行時に引数の指定がなかった場合は，このセルで定義した値 (以下のコードでは `0.1`) がデフォルト値として利用されます。

```python:paramters
alpha = 0.1
ratio = 0.1
```

そして，この変数定義をしたセルをPapermillが認識できるように `parameters` の値を「タグ」として指定します。タグはJupyterノートブックの基本機能としてある，セルのメタ情報を管理する仕組みです。なお，`parameters` のタグを複数のセルに付与しても，Papermillに認識されるのは一つだけ (一番上のセルのみ) である点はご注意ください。

Jupyter Notebookの場合，セルにタグを付与する手順は以下のとおりです。この手順に対応する画面操作は図5を参照ください。

- (1) メニューバーの `View` → `Cell Toolbar` → `Tags` をクリッしてタグ入力欄を追加する
- (2) タグ入力欄にタグ名 `parameters` を入力
- (3) `Add tag`ボタンをクリックして，タグを確定する
- (4) セルの左上に parameters タグの追加を確認したら，
- (5) ノートブックを上書き保存する

> ![papermill_01_tag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/683b64c5-2fb2-ddf6-4e4c-c07f59d3ce21.png)
> ![papermill_02_tag-nb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/edad96f5-409e-f554-4ddf-e5db99f8f759.png)

> **図5. JupyterLabにおけるタグ設定方法**

一方，JupyterLab (Version 2.1.5) の場合は以下の手順でタグを指定できます。この手順に対応する画面操作は図6を参照ください。

- (1) タグを指定したいセルをクリックして選択
- (2) 左サイドバーの プロパティ設定ボタンをクリック
- (3) 左サイドバーの「Add Tag」の欄をクリックして `parameters` を入力したら，
- (4) `+` ボタンをクリックしてタグを確定する。
- (5) `parameters` タグの確定を確認したら，
- (6) ノートブックを上書き保存する

> ![papermill_03_tag-nb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/e0904d80-e123-7047-1971-7c788e74d09c.png)
> **図6. JupyterLabにおけるタグ設定方法**



## 2.3. ノートブックの実行

Papermillでは，PythonAPIとCLIコマンドの2種類のインターフェイスが提供されています。今回は`input.ipynb` のファイル名で保存したノートブックをバッチ実行し，実行結果を`output.ipynb` のファイル名で保存するものとして実行方法を示します。

### (a) PythonAPIを使う方法

まず，PapermillのPythonAPIを利用する場合は，以下のようなコードをPythonスクリプトに埋め込んで実行してください。

```python:PythonAPIによる実行例
import papermill as pm

pm.execute_notebook(
   'input.ipynb',
   'output.ipynb',
   parameters=dict(alpha=5, ratio=7)
)
```

Papermillのexecute_notebook関数は，一番目の引数に実行したいノートブックのファイル名，二番目の引数に保存先のファイル名を受け取ります。また，引数parametersで辞書型の変数を受けており，ノートブック内の`parameters`タグを付けたセルの変数`alpha`と`ratio`の値をそれぞれ`5`と`7`に変更しています。

上記のPythonスクリプトを実行すると，実行結果として `output.ipynb` が保存されます。`output.ipynb`をJupyterで開くと，図7のようにalphaとratioの値が上書きされて実行されていることがわかります。

> ![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/44b92a72-1f4d-8730-c05d-3c4ab7cfbbd8.png)
> **図7. Papermillの実行結果として保存される output.ipynb の中身**

### (b) CLIコマンドを使う方法

次にPapermillのCLIコマンドを使用する方法を示します。Papermillのコマンドでは `-p` オプションで以下のようにパラメータを指定します。また， `--request-save-on-cell-execute`と`--prepare-execute`オプションで保存先のノートブックにセルの実行結果を保存せよと指定しています。

```bash:CLIコマンドによる実行例
papermill input.ipynb output.ipynb \
          --request-save-on-cell-execute --prepare-execute \
          -p alpha 5 \
          -p ratio 7
```

こちらのCLIコマンドでも，実行後にはファイル名 `output.ipynb` で図7のような実行結果が保存されます。

なお，Papermillのコマンドに `--log-output` オプションを追加すれば，セルの実行結果を標準出力に表示します。また，`--progress-bar` オプションを追加すれば実行状況を出力します。どちらも，CLIコマンドとしてノートブックをバッチ実行する際に便利です。

## 2.4. オブジェクトストレージの読み書き

最後に，Papermillのユニークな機能を紹介します。Papermillは図8のように外部オブジェクトストレージ (AWSのS3など) からノートブックを直接に読み書きする機能を持っています。これにより，計算ノードのファイルシステムとして外部オブジェクトストレージをマウントしなくても済むので，ジョブスケジューラで束ねた計算機クラスタやCI/CDシステムでノートブックを実行する際に便利です。

> ![s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/990a7ab5-deb0-c340-5be1-7b02cbbccf5e.png)
> **図8. Papermillの利点2。** AWSのS3のような外部オブジェクトストレージからノートブックを直接に読み書きできるので，s3fsなどと組み合わせて計算ノードにオブジェクトストレージをマウントする操作などの手間はかからない



### オブジェクトストレージの準備

ここでは，AWS S3互換のオブジェクトストレージとしてOSSである[MinIO](https://min.io/)をつかう例を示します。自分でMinIOを起動すれば無償で，お手元のノートPCで利用できるので簡単な試用目的にに適しています。また，MinIOを社内のサーバで起動すれば，お客様からお預かりした機密データや社外秘情報を含むノートブックを社外に送信するリスクを回避できますので，情報管理の観点からも活用する価値があります。

[MinIO開発元が提供しているイメージ](https://hub.docker.com/r/minio/minio/)を使って，MinIOをDockerコンテナとして起動します。コンテナ起動は以下のコマンド実行で可能です。ただし，`ACCESSKEY` と `SECRETKEY` は適宜，ご自分で設定したいパスワードに置き換えてください。

```Dockerfile:MinIOのDockerfile
docker run -p 80:9000 \
  -e "MINIO_ACCESS_KEY=ACCESSKEY" \
  -e "MINIO_SECRET_KEY=SECRETKEY" \
  minio/minio server /data
```

コンテナを起動したら，動作確認のために http://localhost にWebブラウザで接続します。MinIOを起動したサーバとWebブラウザが動作するノートPCが別のサーバである場合，は `localhost` をサーバのIPアドレスで読み替えてください。このとき以下の図9のようにMinIOがパスワード入力を促す画面が表示されれば成功です。このログイン画面でコンテナ起動時に設定した `MINIO_ACCESS_KEY` と `MINIO_SECRET_KEY` の値を入力すればログインできます。

> ![minio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/575b7f6e-d3ac-005e-9127-648f3fd8dcf5.png)
> **図9. MinIOのパスワード入力画面**

MinIOにログインしたら，任意の名称 (ここでは `notebooks` ) でBucketを作成し，バッチ実行したいノートブック `input.ipynb` をアップロードします。

> ![s3_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/bfcc9a48-f711-ef19-329d-0e4b01e7ca9d.png)
> **図10. バッチ実行したいノートブック `input.ipynb` をアップロードした状態のMinIO**

### ノートブックの実行

ここまで準備ができたら，Papermillを使ってノートブックを実行できます。MinIOへの接続情報を環境変数で設定するために，以下のコマンドを実行してください。ただし，パスワードやURLの値はコメントを参考に適宜，お手元の環境に合わせて変更してください。

```bash:環境変数の設定
# 環境変数でパスワードを指定する (Papermillが呼び出すboto3ライブラリの機能)
export AWS_ACCESS_KEY_ID=ACCESSKEY      # docker run時に指定したMINIO_ACCESS_KEYの値に合わせて設定してください
export AWS_SECRET_ACCESS_KEY=SECRETKEY  # docker run時に指定したMINIO_SECRET_KEYの値に合わせて設定してください

# 環境変数で接続先URLを指定する (Papermill限定の機能)
export BOTO3_ENDPOINT_URL=http://localhost:80  # MinIOサーバのIPアドレスとポートを設定してください

# (会社等でHTTP Proxyを利用している場合のみ)
# MinIOに正しく接続するためHTTP Proxyを経由しないように設定する
export no_proxy=localhost
```

後はPapemillコマンドを実行するだけです。以下のように `s3://<バケット名>/<ファイル名>`の形式で入出力用ノートブックを指定して実行すれば，Papermillが自動的にMinIOから `input.ipynb` をダウンロードし，その実行結果を `output.ipynb` としてアップロードします。

```bash:Papermillの実行コマンド
papermill s3://notebooks/input.ipynb s3://notebooks/outout.ipynb \
          -p alpha 5 -p ratio 7
```

papermillコマンドの実行後にMinIOの画面を確認すると，図11のように実行結果 `output.ipynb` がアップロードされていることがわかります。

> ![s3_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541219/06603c64-c86b-f4a3-5f36-ed7a6290cbcb.png)
> **図11. 実行結果のノートブック `output.ipynb` をアップロードした状態のMinIO**


# 3. おわりに

今回は[Papermill](https://papermill.readthedocs.io/en/latest/usage-workflow.html)を使ってノートブックをバッチ実行する方法を示しました。今回のようなツールと，以前紹介した [ジョブスケジューラPBSProを構築して計算ジョブを投入する方法](https://qiita.com/MasafumiTsuyuki/items/acf4b19b54937496e32b)を組み合わせるとパイパーパラメータ探索などを効率的に実行できます。よろしければぜひ試してみてください。

# 4. 参考URL
### Papermillのコンセプトに関して

- [python - Can I run Jupyter notebook cells in commandline? - Stack Overflow](https://stackoverflow.com/questions/35471894/can-i-run-jupyter-notebook-cells-in-commandline)
- [Beyond Interactive: Notebook Innovation at Netflix ](https://medium.com/netflix-techblog/notebook-innovation-591ee3221233)
- [Part 2: Scheduling Notebooks at Netflix - Netflix TechBlog](https://netflixtechblog.com/scheduling-notebooks-348e6c14cfd6)

### Papermillの使い方に関して

- [Jupyter notebook をコマンドラインで実行する - Qiita](https://qiita.com/ikeyasu/items/f6216565bcfee5af96c9)
- [nteract/papermill: 📚 Parameterize, execute, and analyze notebooks](https://github.com/nteract/papermill)
- [Usage  papermill 2.1.1 documentation](https://papermill.readthedocs.io/en/latest/usage-workflow.html)

### PapermillとMinIOを組み合わせて使う方法に関して

- [using with minio · Issue #413 · nteract/papermill](https://github.com/nteract/papermill/issues/413)
- [Configuration — Boto3 Docs 1.14.20 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)