JupyterのDockerイメージまとめ


以下の記事のアップデート版です。Jupyter が5系にバージョンアップしているのと、イメージが整理されています。

- [Jupyter の Docker イメージを使ってみる - Qiita](http://qiita.com/kshigeru/items/2cd504e927869163b4c8)

Jupyter は Python のツールである IPython から出発しましたが、データ分析の過程を Web 上で共有できますので、R でも使われるようになり、最近ではHadoop基盤をバックエンドとして Spark との統合も進んでいます。
Numpy, pandas に代表されるように、データ分析に必要なライブラリは依存関係が複雑化してインストールに苦労することもありますが、Docker を使うことで実行環境のポータビリティを向上できます。また、実行環境をコンテナごとに分離できますので、Hadoop上でDockerを動かす仕組み ([Docker Container Executor - Apache Hadoop 2.7.4](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/DockerContainerExecutor.html)) も検討されています（実用段階にはちょっと早いと思いますが）。
用途に応じて軽量なイメージを使い分ける方向性になると考えられますので、Jupyterで整理されているイメージをまとめておきます。

なお、Jupyter サーバーは単一ユーザーでの利用を想定しています。このため、パスワードは一種類のみの設定となります。複数人で利用するには [JupyterHub](https://jupyterhub.readthedocs.io/en/latest/) を使ってください。


## Dockerイメージの一覧

Jupyter の Docker イメージは以下のように分割して管理されています。GitHubのリポジトリは *jupyter/docker-stacks* ひとつですが、Docker Hub には複数のイメージがあります。

| イメージ名 | 説明 |
|----------|------|
| *base-notebook* | Jupyter Notebook 5.0.x を使えます。科学計算ライブラリは含まれません。 |
| *minimal-notebook* | *base-notebook* に pandoc と texlive などのドキュメント変換ツールが追加されています。 |
| *scipy-notebook* | pandas や scikit-learn など、Python でのデータ分析ライブラリが含まれます。 |
| *datascience-notebook* | *scipy-notebook* に R と Julia が追加されています。R の plyr などは `conda` で管理されています。 |
| *tensorflow-notebook* | *scipy-notebook* に tensorflow が追加されています。GPUのサポートはありません。 |
| *pyspark-notebook* | *scipy-notebook* に Spark 2.2.0 と Hadoop 2.7 が追加されています。Mesos 1.2 のクライアントも含まれます。 |
| *all-spark-notebook* | *pyspark-notebook* に R と Toree および Spylon が追加されています。 |
| *r-notebook* | *minimal-notebook* に R が追加されています。plyr などは `conda` で管理されています。Python も Julia も使わない場合には *datascience-notebook* よりも軽量に用意できます。 |

それぞれのイメージの関係性は GitHub リポジトリの Visual Overview の図 (`internal/inherit-diagram.png`) が分かりやすくなっています。

- [jupyter/docker-stacks: Opinionated stacks of ready-to-run Jupyter applications in Docker.](https://github.com/jupyter/docker-stacks)

起動設定は base-notebook のシェルスクリプトで管理されていますので、オプションなどは "Base Jupyter Notebook Stack" の README を読むのが良いでしょう。

- [docker-stacks/base-notebook](https://github.com/jupyter/docker-stacks/tree/master/base-notebook)

[off topic]
Toree や Spylon を使うと、Spark を使うノートブックを Scala で記述できます。しかし、プロジェクトのステータスとしてはアルファからベータという段階ですので、利用するときはどのバージョンを使っているかに注意する必要があります。
Jupyter にこだわらず、Spark を使うノートブックを構成したい場合には [Apache Zeppelin](https://zeppelin.apache.org/) も良い選択肢になるでしょう。

## *base-notebook* を使う

*base-notebook* を使ってみます。実際にこのイメージで運用することはないと思いますが、約600MBのイメージですので、手早く起動してみる用途にはちょうど良いでしょう。

まずは pull コマンドでイメージをダウンロードします。

```bash
$ docker pull jupyter/base-notebook
$ docker images jupyter/base-notebook
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
jupyter/base-notebook   latest              749eef0adf19        4 days ago          599MB
```

デフォルトの8888番ポートでコンテナを起動します。正常に起動するとコンソールに認証トークンが表示されますので、その URL をコピーしてブラウザでアクセスします。トークンを付けないで Web UI にアクセスし、入力ボックスに *token=* 以降の値を入力しても構いません。

```bash
$ docker run -it --rm -p 8888:8888 jupyter/base-notebook
（省略）
    
    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=1898d7309d85940ac77acc59b15b4f9c572c96afdfc0e198

```

次に、バックグラウンドで実行するには、オプションを変えて実行します。
この場合、コマンド実行後の標準出力には何も表示されません。ノートブックにアクセスするには認証トークンが必要になりますので、 *logs* コマンドを使って起動時のログを確認します。引数には、コンテナ起動時に設定して名前を指定します。

```bash
$ docker run -d --name basenb -p 8888:8888 jupyter/base-notebook
$ docker logs basenb
```

ログの表示だと後から読みにくいという問題があります。 `jupyter` コマンドで、起動しているサーバーの一覧を返すこともできますので、 *exec* コマンドでコンテナにアクセスして `jupyter notebook list` を実行すると分かりやすい出力を得られます。

```bash
$ docker exec basenb jupyter notebook list
Currently running servers:
http://localhost:8888/?token=1898d7309d85940ac77acc59b15b4f9c572c96afdfc0e198 :: /home/jovyan
```

## 認証トークンとパスワード

Jupyter ノートブックサーバーのセキュリティについては以下のドキュメントに記載されています。

- [Security in the Jupyter notebook server](https://jupyter-notebook.readthedocs.io/en/latest/security.html)

認証トークンを使う方法と、パスワードを使う方法の2種類があります。認証トークンは、HTTP の *Authorization* ヘッダーで与える方法と、URL の *token* パラメータで与える方法があります。
デフォルト設定では認証トークンを使う方法が有効化されており、URL の *token* パラメータで与える方法が起動時のメッセージに表示されていると言えます。

認証トークンをデフォルトで有効化する挙動は、Jupyter 4.3 から導入されています。
しかし、ドキュメントを整備し切れないままでのリリースだったようで、いくつかのオプションを組み合わせた場合の誤解も多々見受けられます。

- [Security release: Jupyter Notebook 4.3.1](http://blog.jupyter.org/2016/12/21/jupyter-notebook-4-3-1/)
- [Make token URL clearer when no-browser is given · Issue #1980 · jupyter/notebook](https://github.com/jupyter/notebook/issues/1980)
- [Run notebook without requesting for stupid token · Issue #2254 · jupyter/notebook](https://github.com/jupyter/notebook/issues/2254)

ローカルマシンで起動時に自動的にブラウザを開くようにしている場合は、一時トークン付きのURLにアクセスし、アクセスできたという情報をクッキーに保存するのでシームレスに使えます。しかし、Docker から起動したりリモートサーバーで起動する場合はブラウザの自動起動を無効にしていますので、認証関係に一手間を要することになっています。


認証トークンとパスワードは起動時のオプションで指定できます。

- `--NotebookApp.token`: デフォルト自動生成ではなく、固定の文字列を指定します。設定した値は起動時のログには出力されません。

- `--NotebookApp.password`: URLおよびヘッダーの認証トークンを使わずパスワード認証とします。パスワード文字列は `notebook.auth.passwd` 関数でハッシュ化したものを使います。

原理的には両方に空白を指定すると認証機能を無効化できますが、Webアプリケーションの他のレイヤーでアクセスを制限していない限り、この方法は *強く非推奨* とされています。

[off topic]
設定オプションの処理は [notebook/notebookapp.py](https://github.com/jupyter/notebook/blob/master/notebook/notebookapp.py) で実装されていますので、オプション同士の関係などはソースコードを読むのが正確です。実装には [Traitlets](https://traitlets.readthedocs.io/en/stable/index.html) を使っていますので、リファレンスを開きながら読むと良いでしょう。

`notebook.auth.passwd` 関数は import して引数無しで呼び出すだけです。
指定したいパスワードを入力して、確認用に再入力してください。ハッシュ化した結果を出力します。

![notebook_auth_passwd-on-Try Jupyter.png](https://qiita-image-store.s3.amazonaws.com/0/37996/3cbb67dc-4c42-04de-8ca1-34fe4b1f5734.png "notebook_auth_passwd-on-Try Jupyter.png")

設定ファイルとして保存するには、 `jupyter notebook password` か `python -m notebook.auth password` を実行します。前者の方が良いでしょう。
実行して設定したいパスワードを2回入力すると、 `.jupyter/jupyter_notebook_config.json` に設定ファイルが保存されます。ファイルにしておくと、Dockerコンテナからホストマシンのファイルをマウントすることで管理できます。なお、設定ファイルは Python で管理することも可能です。

```bash
$ docker exec -it basenb jupyter notebook password
$ docker exec -it basenb cat .jupyter/jupyter_notebook_config.json
{
  "NotebookApp": {
    "password": "sha1:2165e2ddd92d:8131245514d60dd9eb91433af30bf1ccbbc36962"
  }
}
```

ここでは、コンテナを再起動させて設定ファイルの内容を反映させます。ブラウザを再読み込みすると、パスワードを入力するテキストボックスが表示されますので、上記で設定したパスワードでログインできることを確認しましょう。

```bash
$ docker stop basenb
$ docker start basenb
```

また、認証トークンが存在しなくなったため、ログにはトークン付きURLが出力されず、 `jupyter notebook list` コマンドでもトークンは表示されません。
これにより、デフォルトの認証トークンから、パスワードに認証に切り替えることができました。

## dockerコマンドからの起動オプションによる設定

起動する度にログを確認したりファイルをマウントすることが適さない環境もあります。そのような場合は起動オプションで認証トークンかパスワードハッシュを与えます。

コンテナを起動するときに `start-notebook.sh` を使い、上述のオプションを指定します。

認証トークンを与える場合：

```bash
$ docker run -d --name basenb-token -p 8080:8888 jupyter/base-notebook start-notebook.sh --NotebookApp.token=foobarbaz
```

パスワードハッシュを与える場合：

```bash
$ docker run -d --name basenb-passwd -p 8088:8888 jupyter/base-notebook start-notebook.sh --NotebookApp.password=sha1:2165e2ddd92d:8131245514d60dd9eb91433af30bf1ccbbc36962
```

認証トークンとパスワードハッシュの両方を与える場合は両方のオプションを付けて実行します。

基本的にはパスワードで管理するのが良いと思いますが、クラウド環境で動かしてURLだけで簡単に共有したい場合は認証トークン方式も良いでしょう。ただし、アクセス元のIPアドレス制限などはユーザー環境のセキュリティポリシーに準じましょう。

その他、SSLの設定などは以下のドキュメントを参照してください。

- [Running a notebook server](http://jupyter-notebook.readthedocs.io/en/latest/public_server.html)


## *scipy-notebook* を使う

起動に関することを整理しましたので、先ほどとは異なる Docker イメージを使ってグラフを描画してみます。

*scipy-notebook* のイメージをダウンロードして容量を確認します。

```bash
$ docker pull jupyter/scipy-notebook
$ docker images jupyter/scipy-notebook
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
jupyter/scipy-notebook   latest              092599e85093        5 days ago          3.91GB
```

バックグラウンドで起動してトークンを確認します。

```bash
$ docker run -d --name scipynb -p 8888:8888 jupyter/scipy-notebook
$ docker exec scipynb jupyter notebook list
```

Webブラウザでノートブックサーバーにアクセスし、Python 3 のノートブックを新規に作成します。

まずは下記のように定型のモジュール読み込みを実行します。

```python:Pythonモジュールの読み込み
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
%matplotlib inline
```

次に、三角関数の正弦と余弦のデータを生成します。

```python:sin/cosの定義
import math
cycle = math.pi * 2
x = np.linspace(-1 * cycle, cycle, 100)
y1 = np.sin(x)
y2 = np.cos(x)
```

matplotlibのAPIで描画してみます。2つのデータ系列を指定し、X軸とY軸に名称を付けてグラフにもタイトルを付けます。日本語を使いたい場合はコンテナ内にフォントファイルを配置してください。 [^1]

[^1]: グラフ軸の日本語設定方法はこちらの記事を参照してください。[matplotlib と Seaborn の軸の日本語設定 - Qiita](http://qiita.com/kshigeru/items/0cfc0778bab197687967)

```python:matplotlibのAPIで描画
sns.set_style('whitegrid')
plt.plot(x, y1, color='red', linewidth=2, label='sin')
plt.plot(x, y2, color='blue', linewidth=1, label='cos')
plt.xlabel('x axis')
plt.ylabel('y axis')
plt.title('sin/cos curve')
plt.legend()
plt.show()
```

![Kobito.RrClEH.png](https://qiita-image-store.s3.amazonaws.com/0/37996/4478b33d-81d8-b475-c0b6-208fd5d02c2a.png "Kobito.RrClEH.png")


同じことをpandasのDataFrameのAPIで描画します。グラフのプロット自体は簡単ですが、軸の設定などは戻り値の `matplotlib.AxesSubplot` を使います。なお、上記のグラフとの違いを分かりやすくするために Seaborn でスタイルを暗めの罫線付きに変更しています。

```python:DataFrameのAPIで描画
df = pd.DataFrame({'sin': y1, 'cos': y2}, index=x)
sns.set_style('darkgrid')
ax = df.plot(title='sin/cos curve')
ax.set_xlabel('x axis')
ax.set_ylabel('y axis')
```

![Kobito.6I3ItZ.png](https://qiita-image-store.s3.amazonaws.com/0/37996/21415faa-1cd4-3783-e030-cffa79896722.png "Kobito.6I3ItZ.png")

続いて、Bokeh を使って株価チャートを描画してみます。株価データは *pandas-datareader* を使って Yahoo! から取得します。 *pandas-datareader* は *pandas.io* パッケージから独立したものです。

モジュールをインストールするには pip を使います。ノートブックからコマンドを実行するには先頭に "!" マークを付けます。

```python:pipを使ってモジュールをインストール
!pip install -y pandas-datareader
```

![ex1.png](https://qiita-image-store.s3.amazonaws.com/0/37996/b561db0b-9e03-fa15-d2aa-a634c200cdc8.png "ex1.png")



日付を指定してデータを取得します。

```python:APIでデータを取得
import datetime
import pandas_datareader.data as web

start = datetime.date(2014, 4, 1)
end = datetime.date.today()

stocks = web.DataReader("^N225", 'yahoo', start, end)
stocks.head(5)
```

![ex2.png](https://qiita-image-store.s3.amazonaws.com/0/37996/1491228e-95a4-90df-a3e1-05b1af07a53d.png "ex2.png")


ノートブックで Bokeh を有効にします。

```python:Bokehを有効化
import bokeh.plotting as bplt
bplt.output_notebook()
```

![ex3.png](https://qiita-image-store.s3.amazonaws.com/0/37996/f07ba40f-9592-6f89-c004-4de0b889ae0e.png "ex3.png")

グラフを描画します。Bokeh は JavaScript を使ってブラウザでレンダリングしますので、コンテナ側に日本語フォントを置かなくても日本語を利用できます。

```python:Bokehでグラフを描画
p = bplt.figure(title='日経平均', x_axis_type='datetime', plot_width=640, plot_height=320)
p.segment(stocks.index, stocks.Open, stocks.index, stocks.Close, color='black')
bplt.show(p)
```

![ex4.png](https://qiita-image-store.s3.amazonaws.com/0/37996/887aee03-8151-c471-6c69-a08a7e482372.png "ex4.png")

## *pyspark-notebook* を使う

PySpark のイメージも使ってみます。稼働中の Spark クラスタに接続するには追加設定が必要となりますが、API の簡単な動作確認などであればスタンドアローンの動作でも十分でしょう。

*pyspark-notebook* のイメージをダウンロードして容量を確認します。

```bash
$ docker pull jupyter/pyspark-notebook
$ docker images jupyter/pyspark-notebook
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
jupyter/pyspark-notebook   latest              26c919b64b68        2 days ago          4.46GB
```

バックグラウンドで起動してトークンを確認します。

```bash
$ docker run -d --name pysparknb -p 8888:8888 jupyter/pyspark-notebook
$ docker exec pysparknb jupyter notebook list
```

Webブラウザでノートブックサーバーにアクセスし、Python 3 のノートブックを新規に作成します。

公式ドキュメントに沿って上から順に実行してみます。なお、ソースコードをクローンしているわけではありませんので、データは個別にダウンロードして使います。

- [Spark SQL and DataFrames - Spark 2.2.0 Documentation](https://spark.apache.org/docs/latest/sql-programming-guide.html)

```python:Sparkセッションの構築
from pyspark.sql import SparkSession

spark = SparkSession \
    .builder \
    .appName("Python Spark SQL basic example") \
    .config("spark.some.config.option", "some-value") \
    .getOrCreate()
```

`wget` コマンドを使ってファイルをダウンロードします。

```python:データのダウンロード    
!wget https://raw.githubusercontent.com/apache/spark/master/examples/src/main/resources/people.json
```

DataFrame API を使ってデータを読み込みます。データとスキーマを表示し、カラム指定でデータを選択できることも確認します。

```python:データの読み込み
df = spark.read.json("people.json")
df.show()
df.printSchema()
df.select("name").show()
```

SQLが使えることも確認します。簡単な集計などはこの方が進めやすいでしょう。

```python:SQLの利用
df.createOrReplaceTempView("people")
sqlDF = spark.sql("SELECT * FROM people")
sqlDF.show()
```

## *tensorflow-notebook* を使う

TensorFlow のイメージも使ってみます。TensorFlow Core の API を試すには GPU 無しでも十分でしょう。

*tensorflow-notebook* のイメージをダウンロードして容量を確認します。

```bash
$ docker pull jupyter/tensorflow-notebook
$ docker images jupyter/tensorflow-notebook
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
jupyter/tensorflow-notebook   latest              374b8bc43218        3 days ago          4.5GB
```

バックグラウンドで起動してトークンを確認します。

```bash
$ docker run -d --name tfnb -p 8888:8888 jupyter/tensorflow-notebook
$ docker exec tfnb jupyter notebook list
```

Webブラウザでノートブックサーバーにアクセスし、Python 3 のノートブックを新規に作成します。

公式ドキュメントに沿って API の動作を確認します。

- [Getting Started With TensorFlow](https://www.tensorflow.org/get_started/get_started)

まずはモジュールをインポートして定数ノードを定義します。

```python
import tensorflow as tf
node1 = tf.constant(3.0, dtype=tf.float32)
node2 = tf.constant(4.0)
node1, node2
```

セッションを定義してノードを評価します。セッションの *run()* メソッドにノードをリストにまとめて与えます。

```python
sess = tf.Session()
sess.run([node1, node2])
```

ふたつの定数ノードに加算操作を適用するノードを定義し、ノードを評価します。

```python
node3 = tf.add(node1, node2)
sess.run(node3)
```

入力を定数ノードではなく、評価時に与えられるようにします。TensorFlow では *placeholder* と呼ばれるデータ型を使います。なお、 `a + b` は `tf.add(a, b)` のショートカットとなっています。

```python
a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b
```

セッションの *run()* メソッドを呼び出すときに *feed_dict* 引数に対して値を指定します。

```python
sess.run(adder_node, {a: 3, b: 4.5})
```

引数をリストで与えると、それぞれの要素に加算操作が適用されます。定数もベクトルも同じように扱えるのは便利ではないでしょうか。

```python
sess.run(adder_node, {a: [1, 3], b: [2, 4]})
```

ノードを接続してセッションで評価する流れは、他の操作に対しても同様です。ただし、小数の演算では端数処理が想定とは異なることがあるかもしれませんので、簡単な計算で動作を確認する場合は注意しましょう。

```python
add_and_triple = adder_node * 3.
sess.run(add_and_triple, {a: [1, 2], b: [3.4, 5.6]})
```

ここまでをノートブックにまとめると以下のようになります。

![tf.png](https://qiita-image-store.s3.amazonaws.com/0/37996/c3a9ce0a-c8c6-d7e3-2daf-0a768422a933.png "tf.png")


公式ドキュメントではここから機械学習の扱い方について進んでいきます。変数を定義して線形モデルを定義したり、訓練データを与えてモデルを構築して実行結果を評価する方法が記載されています。
また、 *tensorflow.examples.tutorials.mnist* モジュールで MNIST の画像認識を試すこともできますので、チュートリアルを順番に読み進めてみると良いでしょう。

## まとめ

Jupyter の Docker イメージが整理されましたので、 *base-notebook*, *scipy-notebook*, *pyspark-notebook*, *tensorflow-notebook* を使ってみました。

データサイエンスで利用できるツールセットは増えてきており、ひとつひとつの管理は大変かもしれませんが、Docker イメージを使うことで初期導入の煩雑さを解消できます。大規模にデプロイしたりパフォーマンスチューニングも懸念として挙げられますが、AWS や GCP などのクラウド環境ではコンテナの利用も簡易化されていますので、ポータビリティの高い再現環境を手軽に作れるようになっています。
用途に応じて公開イメージを使い分けたり、自分で拡張したりと、選択肢の幅が増えているのは良いことだと言えるでしょう。