ChainerUIを使って進捗を綺麗に可視化しよう

# はじめに

こんにちは。
[**株式会社キカガク**](https://www.kikagaku.co.jp/)代表の吉崎です。
<img src='https://qiita-image-store.s3.amazonaws.com/0/130181/profile-images/1506322419' height='150px'>

[**ChainerUI**](https://github.com/chainer/chainerui)が公開され、とうとうChainerもリアルタイムに可視化することができる！と喜びの瞬間でした。
TensorFlowにはTensorBoardが存在していたため、Chainerにもあればなぁ、、、作ろうかなぁ、、、と迷っている段階でもありました。

そういったわけで、便利であることは間違いない**ChainerUI**の使い方を、誰よりも先に紹介していきます。

# 環境構築

まずはGitHubの[**ChainerUI**](https://github.com/chainer/chainerui)のページへアクセスしましょう。

<img width="1013" alt="スクリーンショット 2017-12-20 17.32.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/71a8243b-5ce8-4da3-f6b4-342eda4c9dfe.png">

こちらに記載されている**README.md**の通りに進めていきましょう。

まずは前提として、Chainerがインストールされていることとします。
PythonやChainerのインストール方法は下記の記事で解説しておりますので、こちらを御覧ください。

- [【決定版】WindowsでPythonを使って『機械学習』を学ぶための環境構築](https://qiita.com/yoshizaki_kkgk/items/1057ed4dcc36ed9be7f5)
- [【決定版】MacでPythonを使って『機械学習』を学ぶための環境構築](https://qiita.com/yoshizaki_kkgk/items/4663148a2b3ca078ddbc)

今回はMacで設定を行いますが、基本的にはWindowsの方も同様に進めることができるので、ご安心ください。

## ChainerUIのインストール

まず、ターミナル（Windowsの方はコマンドプロンプトかWindows Powershell）

```bash:chaineruiのインストール
$ pip3 install chainerui
# もしくは pip install chainer
```
<img width="925" alt="スクリーンショット_2017-12-20_17_37_54.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/a297c03d-41b9-aafc-3af6-74310c139236.png">

ここで正しくインストールできているか確認しておきましょう。
コマンドが存在しているかは`which`で確認しましょう（※コマンドプロンプトにはないかもです）。

```bash:インストールされているか確認
$ which chainerui
```

うまくインストールができていれば、下記のようにコマンドのpathが表示され、インストールができていなければ、なにもpathが表示されません。

<img width="514" alt="スクリーンショット_2017-12-20_17_44_04.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/dac0d331-d01b-ebdd-f081-fb4832415125.png">

今回は、うまくpathが表示されたため、インストールが完了していることがわかります。

それでは、まだ設定は終わっていませんが、アプリケーションを使ってみましょう。

```bash:Webサーバーの起動
$ chainerui server
```

これでWebサーバーが起動され、下記の画像のように `http://localhost:5000/` にアクセスすれば良いことがわかります。

<img width="522" alt="スクリーンショット_2017-12-20_17_49_31.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/a9c65c10-f5a5-f48f-1645-63ad4e9702af.png">

Webブラウザでアクセスしてみると、下記のように「There is no project yet.」と出ますが、起動できていることが確認できます。

<img width="1440" alt="スクリーンショット 2017-12-20 17.51.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/0d306b9c-5248-9084-9f35-8db827f068b7.png">

一旦ここで動作確認はおしまいで、`Ctrl + C`でアプリケーションを停止しておきましょう。

ここで、手順書どおり、DBの初期化を行っておきましょう。

```bash:DBの初期化
$ chainerui db create
$ chainerui db upgrade
```

特に何も起こらなかったのですが、DB用のPATHなどが書き込まれたようです。
内部ではSqlite3が動いているようで各プロジェクトごとにディレクトリを作って参照するのではなく、どこかに共通のDBが作られており、そちらを参照しているようです。

# Quick Start
まずはサンプルプロジェクトで良いので、使ってみましょう。
こちらも `README.md` に記載されている内容です。

```bash:お試し
# chaineruiのソースを一式ダウンロード
git clone https://github.com/chainer/chainerui.git

# ダウンロードしたディレクトリに移動
cd chainerui

# 新規プロジェクトの作成
chainerui project create -d examples -n example-project

# Webサーバーの立ち上げ
chainerui server
```

なお、`chainerui project create`の後のオプションは以下のとおりです。

- `-d`：プロジェクトのディレクトリ（`--project-dir`も同じ）
- `-n`：プロジェクトの名前（`--project-name`も同じ）

ここで、`http://localhost:5000`にアクセスしてみましょう。

<img width="1440" alt="スクリーンショット 2017-12-20 18.13.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/30d476b2-503b-1618-5d54-e8b363031079.png">

こちらのように、Projectが始まりました。
それでは、example-projectにアクセスしてみましょう。

<img width="1440" alt="スクリーンショット_2017-12-20_18_13_07.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/efaf7a8d-79ae-82c3-fb22-9160305bb573.png">

こちらのように非常に綺麗なUIが表示されました。感動です。

<img width="1440" alt="スクリーンショット 2017-12-20 18.15.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/5aeba803-eecf-04f9-170c-6bcb5105ea80.png">

基本的には、ChainerのTrainerで書き込むことができるLogReportの`log`を読み込んでいるようなので、日常のプロジェクトにもすぐ使えそうです。

# 簡単な自前のプロジェクトを作成しよう

サンプルプロジェクトで動いて満足しないのが**キカガク流**です。
まずはサンプルプロジェクトを動かし、次に簡単でも良いので自前のデータに対しても動くことを確認していきます。

今回は、ワインの等級をその成分から分類する定番の問題で考えていきましょう。
せっかくなので、少しChainerの使い方も紹介していおきましょう。

## ディレクトリの構成

今回は`test`というディレクトリの中でJupyter Notebookを起動して、Trainerで学習させた結果を`result`というディレクトリの`log`として保存するようにしています。

```
test
├── ChianerUIのテスト.ipynb
└── result
    └── log
```

## 準備

まず最初にディレクトリの作成を行っておきましょう。

```bash:ディレクトリの作成と移動
$ mkdir test
$ cd test
```

そして、Jupyter Notebookを立ち上げましょう。

```bash:JupyterNotebookの起動
$ jupyter notebook
```

これでブラウザ上で`http://localhost:8888`へアクセスし、Juptyer Notebookへアクセスができれば完了です。

## データの読み込み

```python3:UCI機械学習リポジトリのデータセットからデータの読み込み
import pandas as pd

# サンプルデータの置き場所
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data'

# データの読み込み
df = pd.read_csv(url, header=None)
```

こちらで読み込んだデータは以下のようになります。

<img width="609" alt="スクリーンショット 2017-12-20 23.04.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/f6b3e616-dcf6-6c81-0db4-15b3f5afb664.png">

一番左の列がワインの等級（教師データt）であり、それ以外が使用する入力変数xとなります。

```python3:入力変数と教師データの切り分け
# Chainerのラベルは0始まりのため１を引く + numpy int32に変換
t = (df.iloc[:, 0].values - 1).astype('i')

# numpy float32に変換
x = df.iloc[:, 1:].values.astype('f')
```

Pandasをうまく使って切り分けるのですが、Chainerではデフォルトのデータ型が`int32`もしくは`float32`であることに留意して変換したげることと、クラスラベルは0から始めることに気をつけています。

```python3:訓練(train)と検証(test)に分ける
import chainer

# datasetの作成
dataset = list(zip(x, t))

# 訓練（70%）と検証（30%）に分ける
n_train = int(len(dataset) * 0.7)
train, test = chainer.datasets.split_dataset_random(dataset, n_train, seed=0)  # ! シードの固定を忘れないように
```

`chainer.datasets`で準備されている関数を使用して、全体の70%を訓練、３０％を検証データとします。
ちなみに、本日時点で最新版のChainerを使用しており、`chainer.__version__`で調べると、`3.2.0`でした。

## モデルの作成

Chainerで簡単なNNモデルを定義し、インスタンス化しましょう。

まずは必要な主要ライブラリの下記２つを読み込みましょう。

```python3:linksとfunctions
import chainer.links as L
import chainer.functions as F
```

そして、NN用のクラスを定義しましょう。

```bash:NNモデルの定義
class NN(chainer.Chain):
    
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.fc1 = L.Linear(13, 5)
            self.fc2 = L.Linear(5, 3)

    # 順伝播
    def __call__(self, x):
        h = self.fc1(x)
        h = F.relu(h)
        h = self.fc2(h)
        return h
```

とても簡単な `入力層（13ノード） → 中間層（５ノード）　→　出力層（３ノード）`の構成としています。
入力層から中間層の**線形変換**の後に、非線形変換として**Relu関数**も使用しています。

このあたりをもう少し詳しく勉強したい方は、[**ディープラーニングハンズオンセミナー**](https://www.kikagaku.co.jp/services/dnn-seminar/)を覗いてみてくださいね。

インスタンス化して、モデルを宣言します。

```python:モデルのインスタンス化
import numpy as np

# シードの固定
np.random.seed(1)

# NNモデルを宣言
nn = NN()
model = L.Classifier(nn)
```

ここで、モデル内で使用されるパラメータの初期化がランダムに行われるため、シードの固定を忘れないように気をつけてください。
ChainerではNumpyのシードを固定すればOKです。

基本的には、こちらの**順伝播**だけユーザー側で定義しておけば、後はChainer側でパラメータの最適化が行われます。

## モデルの学習

それでは、下記のように`optimizer`を定義したりしながら、**Trainer**を使用して学習を行いましょう。
基本的には、下記のコードを毎回コピペして、適宜必要な箇所だけ変更するようにしています。

```python:モデルの学習
#　最適化のアルゴリズムには SGD を使用
optimizer = chainer.optimizers.SGD()
optimizer.setup(model)

# iteratorsの設定
batchsize = 20
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)

# updaterの設定
updater = chainer.training.StandardUpdater(train_iter, optimizer, device=-1)

# trainerとそのextensionsの設定
from chainer.training import extensions

# trainerの基本設定
epoch = 100
trainer = chainer.training.Trainer(updater, (epoch, 'epoch'), out='result')

# 評価データで評価
trainer.extend(extensions.Evaluator(test_iter, model, device=-1))

# 学習結果の途中を表示する
trainer.extend(extensions.LogReport(trigger=(1, 'epoch')))

# １エポックごとに、trainデータに対するaccuracyと、testデータに対するaccuracyを出力させる
trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy', 'elapsed_time']), trigger=(1, 'epoch'))

# 学習の実行
trainer.run()
```

これを実行すると、途中経過が表示されながら学習が実行されます。

<img width="580" alt="スクリーンショット 2017-12-20 23.26.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/cfbb812e-b520-5dac-1665-41eda7d9e7c2.png">

はい、ここまでが学習です。
こちらの学習結果が`result/log`に保存されているため、確認してみましょう。

## ChainerUIで結果の確認

Jupyter Notebookでターミナルを１つ立ち上げていると思いますが、もう１つ別のターミナルを立ち上げましょう。
今回使用している`test`のディレクトリに移動した後に、まずはChainerUI用のプロジェクトを作成しましょう。

```bash:ChainerUIプロジェクトの作成
$ chainerui project create -d result -n test-project
```

logデータの格納先ディレクトリである`result`をプロジェクトのディレクトリとしておき、名前は適当に`test-project`としました。

こちらのプロジェクトを作成後に、ChainerUIのWebサーバーを立ち上げると、新しいプロジェクトが追加されています。

```bash:Webサーバーの起動
$ chainerui server
```

`http://localhost:5000`にアクセスすると、下記のように、`test-project`が増えていました。

<img width="1440" alt="スクリーンショット_2017-12-20_23_31_18.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/19c5f64c-3a5e-d198-885f-0389eb416414.png">

そして、プロジェクトにアクセスすると下記のように綺麗に表示されていました。
※ 設定で横幅等編集しています。

<img width="1440" alt="スクリーンショット 2017-12-20 23.36.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/1f64e6aa-c9b7-a9ba-cc28-25b2259741cb.png">

とても綺麗！素晴らしいですね！
これで、学習を実行させながらリアルタイムにモニタリングが出来るようになります。
特にGPUマシンを使用するため、クラウド環境が多くローカルよりもデータの可視化に少し手間取るところが解消されますね。

ChainerUI素晴らしいです。

ここまででChainerUIの使い方に関しての解説は終了ですが、そもそも分類の結果が全然良くないため、少しその改善策についてせっかくなので、レクチャーしていきましょう。

# 発展：モデルのカスタマイズを行おう

複数の変数がある場合にあるあるなのが、**スケールによる違いを考慮できていないこと**です。
もう一度データを見てみましょう。

<img width="609" alt="スクリーンショット 2017-12-20 23.04.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/09405237-f4ba-e2fa-1051-1ac57538396b.png">

列番号13の変数では1065, 1050, 1185, ...とスケールが他よりも大きいことがわかると思います。

それにも関わらず、ニューラルネットワーク内の重みは同じスケールでランダムに生成されており、**重み×入力変数の値**が与える影響が大きく異なることがわかると思います。

この問題を解決するには、データの標準化などのスケーリングを行うのですが、前処理として行うよりも、ディープラーニングでは **BatchNormalization** を使用すると非常に楽に実装できるため便利です。
Chainerでは、`L.BatchNormalization`が準備されています。

それでは、BatchNormalizationを使用して、スケーリングの処理も施した状態のニューラルネットワークを学習させてみましょう。

## モデルの定義

```python:BatchNormalizationも考慮したニューラルネットワーク
# モデルの定義
class NN(chainer.Chain):
    
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.fc1 = L.Linear(13, 5)
            self.fc2 = L.Linear(5, 3)
            self.bn1 = L.BatchNormalization(13)  # 追記
            self.bn2 = L.BatchNormalization(5)  # 追記

    # 順伝播
    def __call__(self, x):
        h = self.bn1(x)  # 追記
        h = self.fc1(h)
        h = F.relu(h)
        h = self.bn2(h)  # 追記
        h = self.fc2(h)
        return h
```

## モデルの学習

モデルの定義以降は全く一緒ですので、こちらで学習させていきましょう。

```python3:モデルの学習
# シードの固定
np.random.seed(1)

# NNモデルを宣言
nn = NN()
model = L.Classifier(nn)

#   最適化のアルゴリズムには SGD を使用
optimizer = chainer.optimizers.SGD()
optimizer.setup(model)

# iteratorsの設定
batchsize = 20
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)

# updaterの設定
updater = chainer.training.StandardUpdater(train_iter, optimizer, device=-1)

# trainerとそのextensionsの設定
from chainer.training import extensions

# trainerの基本設定
epoch = 100
trainer = chainer.training.Trainer(updater, (epoch, 'epoch'), out='result')

# 評価データで評価
trainer.extend(extensions.Evaluator(test_iter, model, device=-1))

# 学習結果の途中を表示する
trainer.extend(extensions.LogReport(trigger=(1, 'epoch')))

# １エポックごとに、trainデータに対するaccuracyと、testデータに対するaccuracyを出力させる
trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy', 'elapsed_time']), trigger=(1, 'epoch'))

# 学習の実行
trainer.run()
```

結果が下記のように表示されます。

<img width="571" alt="スクリーンショット 2017-12-20 23.50.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/ceb84107-a7f8-3a4f-9995-fb6157fbba30.png">

今回は**BatchNormalization**の効果によって、かなり大幅に性能が改善できていそうですね。

そして、Trainerによって`log`ファイルが更新されるかと思いきや、同じファイル名の場合は更新されないようですね。

ChainerUIのサーバーを一旦停止して、projectをUI側から削除し、もう一度プロジェクトを作成しました。

```bash:プロジェクトの再作成とWebサーバー起動
$ chainerui project create -d result -n test-project
$ chainerui start
```

<img width="1440" alt="スクリーンショット 2017-12-21 0.01.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/130181/1f0fb5fc-cbe1-d44e-ce8b-74e0cfa7816f.png">

そうすると、上記のようにとても綺麗な画面が表示されました。
おそらく、プロジェクトの再作成とWebサーバー再起動は非常に効率が悪い方法であるため、試していただいた方で効率良く再読込できる方法を見つけた方はコメントお願いします。

# おわりに

**ChainerUI**便利ですね。
非常に綺麗に表示されるので、お客様先の満足度が上がること間違いなしです。
とても良いサービスをありがとうございます。

みなさん、ChainerUIでハッピーなChainerライフを送りましょう！

# 参考
[Chainerの実験に対してログを可視化したり外部制御するExtension＆WebアプリをPFNで作った](https://qiita.com/colspan/items/7e9ba3d56c8ffaccdc70)

# 著者紹介

<img src='https://qiita-image-store.s3.amazonaws.com/0/130181/profile-images/1506322419' width='150px'>
株式会社キカガク
代表取締役社長　吉崎 亮介（twitter: [@yoshizaki_kkgk](https://twitter.com/yoshizaki_kkgk)）

一緒に機械学習の教育を推進してくださるメンバーを募集しています！
ぜひ、[Twitter](https://twitter.com/yoshizaki_kkgk)からメッセージお待ちしています！