Jupyter notebook を管理しやすくする Jupytext を使ってみた

皆さん、Jupyter notebook は使ってますか?
今回は [Jupytext](https://github.com/mwouts/jupytext) という Jupyter notebook を管理しやすくなるツールを見つけたので使ってみました。
控えめに言ってめっちゃ便利なのでは！という感じだったので、しばらく使ってみようと思いつつ、簡単に紹介記事を書きました。

まだ使い始めなので、間違ってるよ！とかこうするともっと便利！などなど、ご指摘歓迎です :bow: 

# Jupytext とは

Jupyter notebook をより便利にするツールです。

Jupyter notebook は既に広く使われているツールですが、状態を保持したままトライアンドエラーがしやすく便利な反面、下記のように課題もあります。

- Jupyter notebook 自体は便利だけど、コードを書くときはIDEなど別の使い慣れたエディタを使いたい
- `.ipynb` のファイルの中身は json で差分が見づらく、バージョン管理が難しい
- `.ipynb` のファイルは (マージが難しいため)共同編集しづらい

Jupytext は、上記の解決を目指したツールで、具体的には次のようなことが出来ます。

- `.ipynb` ファイル作成時、同時に `.py` ファイルも作成される
- 普段使っているエディタで `.py` ファイルを編集することで、`.ipynb` ファイルのセルを修正できる(逆も可)
- `.ipynb` の差分を通常の python スクリプトとして見やすい形で git に取り込める
- つまり git でマージもしやすい

めっちゃ便利そう！

[Jupytext](https://github.com/mwouts/jupytext) の README にある表です。`Paired notebook` が Jupytext にあたるところで、notebook上では出力や状態を保ったまま、スクリプトの部分は `.py` ファイルとして管理できます。

| Format | Extension | Text editor friendly | IDE friendly | Git friendly | Preserve outputs |
|:--|:--|:--|:--|:--|:--|
| Jupyter notebook | .ipynb |  |  |  | ✔ |
| Markdown or R Markdown | .md/.Rmd | ✔ |  | ✔ |  |
| Julia, Python or R script | .jl/.py/.R/... | ✔ | ✔ | ✔ |  |
| Paired notebook | (.jl/.py/.R/.../.md/.Rmd) + .ipynb | ✔ | (✔) | ✔ | ✔ |![)

# インストール & 設定

pipenv を使ってますが、適宜 pip に読み替えてもらって問題ないはず。

```sh
$ pipenv install jupyter
$ pipenv install jupytext
# .jupyter/jupyter_notebook_config.py をまだ作っていない場合は下記を実行でファイル作成
$ pipenv run jupyter notebook --generate-config
$ vi ~/.jupyter/jupyter_notebook_config.py
# 下記をファイル末尾に追加して jupyter 起動
c.NotebookApp.contents_manager_class = "jupytext.TextFileContentsManager"
$ pipenv run jupyter notebook
# 起動するのを確認
```

Jupyter Notebook が起動したら、適当な notebook を開き Edit > Edit Notebook Metadata に `"jupytext": {"formats": "ipynb,py"},` を追加し、notebook を保存します。

![jupytext-try_01.png](https://qiita-image-store.s3.amazonaws.com/0/49409/9210b0b2-9117-1686-2687-375954eb2bb0.png)
![jupytext-try_02.png](https://qiita-image-store.s3.amazonaws.com/0/49409/6c9da16e-ace9-9e0e-410a-f1cf0e6a92a5.png)



`.py`, `.ipynb` 以外にも、`md`, `Rmd`, `jl`, `R` などのフォーマットが使えるようです。

notebook を開くたびにこれを編集するのは面倒なので、 config ファイルに書いておくと新たに作成するファイルには自動で付加されるようになります。(既存ファイルは手動で入れる必要があるようです)

```sh
$ vi ~/.jupyter/jupyter_notebook_config.py
# 下記をファイル末尾に追加して jupyter 起動
c.ContentsManager.default_jupytext_formats = "ipynb,py"
```

# 使ってみる

下記の mnist を DNN で学習しているだけの Jupyter notebook (`.ipynb`) があったので、それで試してみます。
https://github.com/cfiken/jupytext-try/blob/master/mnist_dnn.ipynb
(雑なものなので中身はスルーしてください :scream_cat: )

上記を Jupyter notebook 上から開いて metadata を編集し、上書き保存してブラウザを更新します。
Jupyter notebook のファイル一覧を見ると、 `mnist_dnn.py` が作成されているのが確認できます。

![jupytext-try_03.png](https://qiita-image-store.s3.amazonaws.com/0/49409/8d56f24a-f50c-d673-635a-de71e8f16536.png)



あとで差分を見やすくするために、一度 `mnist_dnn.ipynb` `mnist_dnn.py` を git commit しています。その上で、下記をそれぞれ試してみます。

- `.ipynb` ファイル側を Jupyter notebook で編集
- 作成された `.py` ファイル側を適当なエディタで編集



## `.ipynb` ファイル側を Jupyter notebook で編集

Jupyter notebook 上で `mnist_dnn.ipynb` で使用している `num_units` を変えて保存してみます。
`num_units = 256` としていたところを、 `num_units = 128` に変えてみました。
該当部分の差分は下記。このくらいの差分なら `.ipynb` でも問題ないですね。

```diff
diff --git a/mnist_dnn.ipynb b/mnist_dnn.ipynb
index 632b464..0336bc2 100644
--- a/mnist_dnn.ipynb
+++ b/mnist_dnn.ipynb
@@ -67,7 +67,7 @@
     "# constant\n",
     "\n",
     "num_inputs = 784  # 28*28\n",
-    "num_units = 256\n",
+    "num_units = 128\n",
     "num_outputs = 10\n",
     "num_layers = 1\n",
     "batch_size = 64\n",
```

全体の差分で見ると、同時に `mnist_dnn.py` も自動で更新されているのがわかります！

```diff
diff --git a/mnist_dnn.py b/mnist_dnn.py
index 6e5d73f..fc1a771 100644
--- a/mnist_dnn.py
+++ b/mnist_dnn.py
@@ -62,7 +62,7 @@ test_labels = mnist.test.labels
 # constant

 num_inputs = 784  # 28*28
-num_units = 256
+num_units = 128
 num_outputs = 10
 num_layers = 1
 batch_size = 64
```

## 作成された `.py` ファイル側を適当なエディタで編集

`mnist_dnn.py` を別の editor で開いてみた。全部載せると長いので、先程修正した `num_units` 付近まで載せます。
(全体を見たい方は [github](https://github.com/cfiken/jupytext-try/blob/master/mnist_dnn.py) をご覧ください。)

```py3
# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py:light
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.3'
#       jupytext_version: 0.8.1
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
#   language_info:
#     codemirror_mode:
#       name: ipython
#       version: 3
#     file_extension: .py
#     mimetype: text/x-python
#     name: python
#     nbconvert_exporter: python
#     pygments_lexer: ipython3
#     version: 3.6.5
#   ... 省略 ...
# ---

# + {"ExecuteTime": {"end_time": "2018-05-02T03:57:35.459055Z", "start_time": "2018-05-02T03:57:32.757602Z"}}
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.examples.tutorials.mnist import input_data

# + {"ExecuteTime": {"end_time": "2018-05-02T03:57:36.012133Z", "start_time": "2018-05-02T03:57:35.461307Z"}}
mnist = input_data.read_data_sets("data/", one_hot=True)
test_images = mnist.test.images
test_labels = mnist.test.labels

# + {"ExecuteTime": {"end_time": "2018-05-02T03:58:04.194738Z", "start_time": "2018-05-02T03:58:04.188221Z"}}
# constant

num_inputs = 784  # 28*28
num_units = 256
num_outputs = 10
num_layers = 1
batch_size = 64
learning_rate = 0.001
num_epocs = 5000
step_to_print = 10
```

中を見てみると、最初に Jupyter notebook の metadata がズラッと書かれた後、セルごとに改行で区切られているようです。
`# + {"ExecuteTime": ...}` というのは jupyter の拡張 (nbextension) で入れている計算時間を表示する plugin の影響で、何もない場合は空行だけになります。

今度はこちらの中身を変更してみます。
先程変更した `num_units = 128` の部分を、 `num_units = 256` に戻して保存します。
このまま Jupyter notebook 側に戻り、先ほど開いていた `mnist_dnn.ipynb` を(ブラウザを)更新すると、notebook 上で内容が反映されました！
保存すると次のような差分になります。

```diff
diff --git a/mnist_dnn.ipynb b/mnist_dnn.ipynb
index 0336bc2..39f1cd4 100644
--- a/mnist_dnn.ipynb
+++ b/mnist_dnn.ipynb
@@ -67,7 +67,7 @@
     "# constant\n",
     "\n",
     "num_inputs = 784  # 28*28\n",
-    "num_units = 128\n",
+    "num_units = 256\n",
     "num_outputs = 10\n",
     "num_layers = 1\n",
     "batch_size = 64\n",
diff --git a/mnist_dnn.py b/mnist_dnn.py
index fc1a771..6e5d73f 100644
--- a/mnist_dnn.py
+++ b/mnist_dnn.py
@@ -62,7 +62,7 @@ test_labels = mnist.test.labels
 # constant

 num_inputs = 784  # 28*28
-num_units = 128
+num_units = 256
 num_outputs = 10
 num_layers = 1
 batch_size = 64
```

`.py` ファイル側の変更が、`.ipynb` 側にも反映されていますね。

## 実行してみる

Jupyter notebook 上で実行してみると、`.ipynb` ファイル側には出力部分も json として保存されるため大量の差分が出ますが、 `.py` ファイル側には差分は乗りません。
今まで実行するたびに100以上のdiffが出ていたのですが、 `.py` ファイルだけを見れば差分もスッキリしますね。

# まとめ

- Jupytext は Jupyter notebook のファイル (`ipynb`)のスクリプト部分だけ別の `.py` ファイルで管理できる
- どちらでも更新できて、 `.py` だけ見れば差分もスッキリ

ファイル数が増える以外はそんなにデメリットがないので、とりあえず入れておいても良さそうな感じです。いつかバージョンを戻したい時に役に立つかも...!
また使ってみて何かあれば書いてみたいと思います :star2: