PyCharmのリモートインタプリタ機能でDeep Learningを手軽にできる開発環境を構築してみた

今回はPyCharmのリモートインタプリタ機能をつかって、Deep Learningを手軽にできる開発環境を構築をしたところ、思った以上に開発が捗ることがわかったので、紹介したいなと思います。

# 目的

Deep Learningの開発についてMacのようなマシンを使っても、Deep Learningの開発を手軽にやる方法をさがしていました。

当たり前ですがノートPCだとマシンパワーが全然たりないので、GPUの入ったデスクトップマシンを買って、そこにUbuntu入れてやるのが普通かもしれません。ただ、いかんせんデスクトップなので、どこでも開発できるわけじゃありません。

もちろんMacからでもGPUマシンにsshさえ接続できれば、ファイルをGPUマシンに対してアップロード、そこで実行させればできますが、手軽なもんじゃないです。

IDEで開発しつつ、GPUマシンで直接実行できる方法が無いかと思い色々探した結果、PyCharmのProfessional機能にはリモートインタプリタ機能があることをみつけました。

どうやら、この機能を使うことでsshでPythonの開発環境を読み込み、それをライブラリとして開発に利用できたり、動作確認時にはリモート先でテストできるようになるみたいです。

つまり、開発マシンの環境を汚さず、リモートで読み込んだマシンのPython環境をベースで開発できるわけです。当然コードジャンプ・補完機能・さらにリファクタリング機能も使えちゃうので言うことありません。

一方、Deep Learning系のライブラリは(tensorflowのpythonは安定したといっているものの）ライブラリの更新が激しく、そのたびにGPUマシンの環境を汚したくないということで、nvidia-dockerをつかっていました。

これを組み合わせてつかえば、双方の環境を汚す心配もなく、Deep Learningの開発が手軽にできるんじゃないかと思い、頑張って構築してみました。

# 実現するのに使った環境

## Deep learningを動かすマシン環境
  * OS: Ubuntu 16.04 LTS
  * GPU: GTX 1080
  * 実際に使用したソフトウェア
      * Docker 1.12.0
      * DockerCompose 1.11.2
      * nvidia-docker V1.0.1

## 開発に使ったマシン環境
  * OS: Mac OS X 10.10.5
  * PyCharm Professional 2017.3

# 環境の構築方法

## Deep learningを動かすマシン上でsshで接続できるコンテナ環境を作っていく

PyCharmのリモートインタプリタ機能を使うためには、sshを接続できるコンテナを作る必要があります。そこで、まずはリモートでコンテナを動かせるようにしました。

適当な場所にフォルダを作り、このような配置で作っていきます。

```
├── docker-compose.yml
└── tensorflow
    └── Dockerfile
```

### sshに接続できるDockerfileの作成

sshに接続でき、Deep Learningを動かせるDockerfileを作成していきます。場所は、`tensorflow/Dockerfile`とします。

今回はtensorflow 1.0.1 + keras 2.0.1のコンテナを作ることにします（もちろんchainerでも同じようにすればできるはずです）。

コンテナ上にsshをインストールするのは、セキュリティ的にどうかという意見はあるかもしれませんが、そこについては今回考えないことにします。

``` Dockerfile:Dockerfile
FROM tensorflow/tensorflow:1.0.1-gpu

# sshをインストールしています
RUN apt-get update && apt-get install -y git openssh-server

RUN ( echo "pandas==0.19.2"; \
      echo "h5py==2.7.0"; \
      echo "keras==2.0.1"; \
    ) > requirements.txt && \
    pip install -r requirements.txt && rm -rf requirements.txt

# ここでは次のことをやっています
# sshサービスを起動させ、tail -fでコンテナを動かし続けるための起動スクリプトを作成
# rootでsshにログインできるようsshの設定を変更。さらにsshでコマンド実行しても環境変数を追加できるようオプションを変更
# sshでコマンド実行しても、sshに入ってもGPUが使えるようにするため、環境変数の設定の追加
RUN ( echo "#!/bin/bash"; \
      echo ""; \
      echo "service ssh start"; \
      echo "tail -f /dev/null"; ) > /root/entrypoint.sh && \
    chmod +x /root/entrypoint.sh && \
    sed -i.bak 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
    echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_config && \
    ( echo ""; \
      echo "export PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:$PATH"; \
      echo "export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:"; \
      echo "export LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64"; \
    ) >> /root/.bashrc && \
    mkdir /root/.ssh && chmod 700 /root/.ssh && \
    ( echo "PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:$PATH"; \
      echo "LIBRARY_PATH=/usr/local/cuda/lib64/stubs:"; \
      echo "LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64"; \
    ) >> /root/.ssh/environment


EXPOSE 22

CMD ["/root/entrypoint.sh"]
```

### GPUを動かすためのdocker-compose.ymlファイルを作成

つぎに`docker-compose.yml`ファイルを作成します。

ここでは、GPUやPyCharmを動かすための工夫をいくつかやっています（工夫している内容はコメントに記載してます）。

``` yaml:docker-compose.yml
version: '2'
services:
  machine_learning:
    build: ./tensorflow
    volumes:
      # nvidia-docker起動時に生成されるボリュームを呼び出しています
      # nvidia-dockerではこのボリュームを内部で呼び出して実現しています。
      # また、367.57の数字はNvidiaドライバのバージョンです。
      # 実際に入っているドライバのバージョンによってボリュームで指定しているバージョンが変わります。
      # ここは現在入っているドライバのバージョンを見るか、ボリュームの一覧をみてなんのボリューム入っているかみて設定してください
      - nvidia_driver_367.57:/usr/local/nvidia:ro
      # 実行結果やモデルをホスト側に配置したいため設定しています。
      # コンテナ上で/shareにデータを保存すれば、ホストにデータが保存されるようになります。
      - ./share:/share:rw
      # pycharmでリモート接続したとき、Pycharmのヘルパーが配置されます。
      # それをコンテナ立ち上がるたびに毎回入れるのを避けるために設定しています。
      - ./.pycharm_helpers:/root/.pycharm_helpers:rw
    # ここではGPUまわりのデバイス設定をやっています。この設定もnvidia-dockerはやっています。
    devices:
      - "/dev/nvidiactl"
      - "/dev/nvidia-uvm"
      - "/dev/nvidia0"
    tty: true
    stdin_open: true
    # sshを外部から接続できるようにするため、ポート設定をしています
    ports:
      - "8022:22"

# ここでは、nvidia-docker起動時に生成されるボリュームを呼び出します
volumes:
  nvidia_driver_367.57:
    external: true
```

### コンテナを立ち上げる

ここまで作れば、`docker-compose`コマンドを実行して、イメージをビルドし、そのままコンテナを立ち上げましょう。

```shell-session
$ docker-compose build
$ docker-compose up -d
```

## コンテナに入ってルートのパスワードを設定する

（個人的にここは改善したいところですが）rootでログインするためにはパスワードの設定が必要になります。

そこでコンテナに入ってコンテナのrootユーザのパスワードの設定をします。コンテナのフルネームについては`docker ps`で確認できますので、コンテナ名を確認してコンテナに入りましょう。

```shell-session
$ docker exec -it {フォルダ名}_machine_learning_1 bashrc
```

そしてコンテナ上でrootのパスワードを変更します。

```
# パスワード設定変更
$ passwd
```

### 入れるか確認する

ここまでできれば、sshでコンテナに入れるはずです。実際に入れるか確認します。

```shell-session
$ ssh -P 8022 root@localhost
```

また、開発側のマシンからもsshで入れるかも見ましょう。入れなかったらPyCharmで設定できないので、必ずチェックしましょう。

```shell-session
$ ssh -P 8022 [開発側のホストIP]@localhost
```

## PyCharmで先程作ったコンテナにリモートで接続できるようにする

ここまでできればあとはPyCharmからコンテナにリモート接続できれば開発できるようになります。その設定をしてきましょう。

※ PyCharmのバージョンによっては設定する場所とか変わったりする可能性があります。もし見つからない場合は、それらしき設定を探しましょう。。。

### Pycharmで先程起動させたコンテナをリモートで指定する

まずはコンテキストメニューからPyCharm -> Preferencesと選択していきます。

つぎにProject: プロジェクト名 -> Project Interpreterを選択します。

するとこの画面がでてきます。

<img width="1000" src="https://qiita-image-store.s3.amazonaws.com/0/95464/39027bad-db0c-adf3-eb5f-631b3d13aea8.png">

そこで、Project Interpreter横の...ボタンをクリックし、Add Remoteを選択します。

ここで新規作成画面がでてきます。

<img width="749" src="https://qiita-image-store.s3.amazonaws.com/0/95464/e3cc65aa-6c50-fe60-0f7c-e25e223fcae7.png">

ここではつぎのようにしました。

* `SSH Credentials`を選択
* `Host`はDeep learningを動かすマシンのIP(設定する際、実際の社内のホストを設定しているので、ここはモザイクしています）
* `Port`は`docker-compose.yml`でポート設定をした8022を入力
* `User name`はrootと入力
* `Auth type`はpasswordを選択
* `Password`はコンテナ上で設定したrootのパスワードを入力
* `Python Interpreter path`はデフォルトのまま

これで問題なければOKをおしましょう。

最初はPyCharmのヘルパーがコンテナ上にインストールされますが、しばらく待ちましょう。

上手く行けばこのようにコンテナ上でインストールされたパッケージがでてくるはずです。

<img width="1208" src="https://qiita-image-store.s3.amazonaws.com/0/95464/cc7d3ece-7018-d0fa-bcf9-e44bae181ccd.png">

ここにtensorflowやkerasのライブラリなどが入っているかどうかは確認しときましょう。

### Project Mappingの設定をする

つぎにProject Mappingの設定も設定する必要があるのでそのまま設定していきましょう。

まずは、先程の画面のProject mappings横の...ボタンをクリックします。

そこでは、Local PathとRemote Pathの２つを指定しますので、LocalPathはプロジェクトのパスを、Remote Pathは適当な場所（今回は/root/projects/tensorflow-projectsにしました）に設定します。

### 動かしてみる

これでリモートでコンテナが動かせるようになりました。

ここまでできれば、こんな感じにコンテナのライブラリをもとに補完も効いてくれるので、開発しやすくなっていると思います。

<img width="632" src="https://qiita-image-store.s3.amazonaws.com/0/95464/8838d78b-f15a-f8f7-37c9-7123fd81c6c3.png">

では、適当にプログラムをつくって、コンテナでプログラムを実行してみましょう。今回は試しにこんなものを作ってみました。

足し算を学習できないか確かめたものですが、あまり精度は良くないですｗ数値の学習はもう少し工夫が必要ですかね？

（いかんせん初めたばっかなので、ここはあまり突っ込まないでくださいｗｗ

``` python:test.py
import random

from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout

import numpy as np
import os

def generate_train_data(amount):
    x_train = np.empty((0,2), int)
    y_train = np.array([])
    for num in range(amount):
        x = random.randint(1, 1000)
        y = random.randint(1, 1000)
        z = (x + y)
        x_train = np.append(x_train, np.array([[x, y]]), 0)
        y_train = np.append(y_train, z)
    return x_train, y_train

def model_add():
    model = Sequential()
    model.add(Dense(64, input_dim=2))
    model.add(Activation("relu"))
    model.add(Dropout(0.2))
    model.add(Dense(128))
    model.add(Activation("relu"))
    model.add(Dropout(0.2))
    model.add(Dense(64))
    model.add(Activation("relu"))
    model.add(Dropout(0.2))
    model.add(Dense(units=1))

    return model

(X_train, Y_train) = generate_train_data(10000)

print(X_train)
print(Y_train)

model = model_add()

if os.path.exists("/share/model_add.h5"):
    model.load_weights("/share/model_add.h5")

model.compile(loss='mean_absolute_error', optimizer='adam', metrics=['accuracy'])

model.fit(X_train, Y_train, epochs=500)

print(model.predict(x=[[30,40],[40,60],[120,140], [400, 200]]))

model.save_weights("/share/model_add.h5")
```

テストする際はデプロイする必要があるので、先程作ったファイルを右クリックし、Upload to projectと選択しましょう。

うまくいけば、「Upload to project completed in less than a minute: 1 file transferred (696 b/s)」とかでてきます。

フォルダはなければ自動的に作ってくれるようですので、コンテナ側でフォルダを作る必要はありません。

もし、ここでエラーがでたらSSHの設定等を見直してみてください。

これでコンテナにプログラムファイルがアップロードされたので、あとはファイルを実行してあげましょう。

うまくいけば、コンソール上でGPUマシンのコンテナに接続されてプログラムを実行していることが確認できます。

<img width="1572" src="https://qiita-image-store.s3.amazonaws.com/0/95464/fde67e75-2e7e-9360-070b-27211bda2d00.png">

これで環境ができあがりました。

# まとめ

途中で気づく方がいるかと思いますが、実はPyCharmにはDockerコンテナやDocker Composeのファイルを指定できます。

これらの機能の便利さも知っていたので、最初はこちら側の機能をつかってやろうとしました。

ところが、両方共ローカルで実行することが前提になっているのか、上手く動かせませんでした。。。

なので仕方なくsshのリモートを使ったわけなのですね。。。

PyCharmのDockerやDocker Composeでもリモートで利用できるようになればいいですね！（強いて言えばsshいれなくてもできるようになると、よりよいですねｗ