TensorFlowのLeNet-5 MNIST, AlexNet MNISTをAWS EC2 t2.micro(無料利用枠対象)で実行してみる

# はじめに
TensorFlow（テンソルフロー）は、Googleが開発したディープラーニングのシステムでApache 2.0ライセンスで公開されています。
GPU対応、C++, Pythonに対応しています。

「GPUを使ってバリバリ動かしたぜ！」

といった投稿が多いので、あえて、AWS EC2 t2.micro(無料利用枠対象)にTensorFlow(Python版, Anaconda)をインストールし実行してみました。
AWS EC2 T2インスタンスのCPUクレジットについて理解の手助けになれば幸いです。

# LeNet-5, MNIST
今回は、サンプルプログラム
tensorflow/models/image/mnist/convolutional.py
を動作させます。
モデルのLeNet-5はこんな感じです。（[出典](http://www.dataiku.com/blog/2015/08/18/Deep_Learning.html)）
![le_net.png](https://qiita-image-store.s3.amazonaws.com/0/107056/e2a8b553-4474-dbc8-4872-14ca82a10bc2.png)

MNISTは、手書きの数字0～9のデータセットです。（[MNIST DATABASE](http://yann.lecun.com/exdb/mnist/)）
学習データが60000個、評価データが10000個あります。
![MNIST.png](https://qiita-image-store.s3.amazonaws.com/0/107056/4a76d37e-cbfe-84ae-220e-666589d2ae6a.png)

LeNet-5, MNISTともに、こちらの論文で発表されています。
> [LeCun et al., 1998]
> Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner. Gradient-based learning applied to document recognition. Proceedings of the IEEE, november 1998

今回のTensorFlowのサンプル（convolutional.py）を使うと、99.2%程度の正解率を達成することができます。
※ scikit-learnのsvmでMNISTの学習を行うと、正解率は97%程度です。ただし、ディープラーニングは学習時間が何十倍、何百倍かかります。

# AWS EC2にTensorFlowをインストール
### 1. WS EC2
Ubuntu Server 16.04 LTS (HVM), SSD Volume Type ⇒ t2.micro
を選択します。

### 2. Puttyの設定（Windowsの人向け）
+ Session > Host Name  
    ubuntu@[public_dns_name]
+ Connection > SSH > Auth > Authentication parameters > Private key file for authentication  
    [*.ppk file]

### 3. Anaconda + TensorFlow
後々、Pythonをバリバリ使う場合に備えて、Anaconda上にTensorFlowをインストールします。

+ **Anacondaダウンロード**
    
    ```
    $ mkdir tensorflow
    $ cd tensorflow
    $ curl https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh -o Anaconda3-4.2.0-Linux-x86_64.sh
    ```
+ **Anacondaインストール**
    ライセンスへの同意、インストールパス、.bashrcにPATHを設定するか聞かれます。
    
    ```
    $ bash Anaconda3-4.2.0-Linux-x86_64.sh
    >>> (ENTER)
    >>> yes
    [/home/ubuntu/anaconda3] >>> (ENTER)
    PATH in your /home/ubuntu/.bashrc ? [yes|no]
    [no] >>> yes
    ```
    インストールが終わったら、PATHを認識させるために、ログアウト＆ログイン します。
    Anacondaの動作確認をします。
    
    ```
    $ conda -V
    conda 4.2.9
    ```
+ **TensorFlowインストール**
    tensorflow環境をAnacondaに作成します。
    
    ```
    $ conda create -n tensorflow python=3.5
    Proceed ([y]/n)? y
    ```
    Anacondaのtensorflow環境にTensorFlowをインストールします。
    
    ```
    $ source activate tensorflow
    (tensorflow)$ conda install -c conda-forge tensorflow
    
    The following NEW packages will be INSTALLED:
    
        mkl:        11.3.3-0
        mock:       2.0.0-py35_0   conda-forge
        numpy:      1.11.2-py35_0
        pbr:        1.10.0-py35_0  conda-forge
        protobuf:   3.0.0b2-py35_0 conda-forge
        six:        1.10.0-py35_0  conda-forge
        tensorflow: 0.10.0-py35_0  conda-forge
    
    Proceed ([y]/n)? y
    ```
    
    ちなみに、AnacondaのTensorFlow環境を終了させるコマンドはこちら。
    
    ```
    (tensorflow)$ source deactivate
    ```
    次回以降、AnacondaのTensorFlow環境を開始するコマンドはこちら。
    
    ```
    (tensorflow)$ source activate tensorflow
    ```
    
    TensorFlow環境に切り替えないと、TensorFlow実行時に下記エラーがでます。
    
    ```
    >>> import tensorflow as tf
     Traceback (most recent call last):
       File "<stdin>", line 1, in <module>
     ImportError: No module named 'tensorflow'
    ```
    TensorFlowがインストールされたディレクトリの確認。
    
    ```
    (tensorflow)$ python -c 'import os; import inspect; import tensorflow; print(os.path.dirname(inspect.getfile(tensorflow)))'
    /home/ubuntu/anaconda3/envs/tensorflow/lib/python3.5/site-packages/tensorflow
    ```
+ **TensorFlow動作確認**
    Hellow TensorFlow!とTensorFlowで計算をしてみます。
    
    ```
    $ source activate tensorflow
    (tensorflow)$ python
    ...
    >>> import tensorflow as tf
    >>> hello = tf.constant('Hello, TensorFlow!')
    >>> sess = tf.Session()
    >>> print(sess.run(hello))
    Hello, TensorFlow!
    >>> a = tf.constant(10)
    >>> b = tf.constant(32)
    >>> print(sess.run(a + b))
    42
    ```
+ **TMNIST動作確認**
    
    ```
    (tensorflow)$ python /home/ubuntu/anaconda3/envs/tensorflow/lib/python3.5/site-packages/tensorflow/models/image/mnist/convolutional.py --self_test
    Running self-test.
    Initialized!
    Step 0 (epoch 0.00), 6.7 ms
    Minibatch loss: 9.772, learning rate: 0.010000
    Minibatch error: 92.2%
    Validation error: 0.0%
    Test error: 0.0%
    test_error 0.0
    ```
    
    以上で、準備は整いました。

# AWS EC2 t2.microでLeNet-5 MNISTを実行
それでは、実行してみます。

```
(tensorflow) $ python /home/ubuntu/anaconda3/envs/tensorflow/lib/python3.5/site-packages/tensorflow/models/image/mnist/convolutional.py
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting data/train-images-idx3-ubyte.gz
Extracting data/train-labels-idx1-ubyte.gz
Extracting data/t10k-images-idx3-ubyte.gz
Extracting data/t10k-labels-idx1-ubyte.gz
Initialized!
Step 0 (epoch 0.00), 6.8 ms
Minibatch loss: 12.053, learning rate: 0.010000
Minibatch error: 90.6%
Validation error: 84.6%
Step 100 (epoch 0.12), 432.6 ms
Minibatch loss: 3.276, learning rate: 0.010000
Minibatch error: 6.2%
Validation error: 7.2%
Step 200 (epoch 0.23), 435.2 ms
Minibatch loss: 3.457, learning rate: 0.010000
Minibatch error: 14.1%
Validation error: 3.9%
Step 300 (epoch 0.35), 430.3 ms
Minibatch loss: 3.204, learning rate: 0.010000
Minibatch error: 6.2%
Validation error: 3.1%
Step 400 (epoch 0.47), 431.9 ms
Minibatch loss: 3.211, learning rate: 0.010000
Minibatch error: 9.4%
Validation error: 2.5%
```
順調に学習が進んでいきます。
100ステップごとに状況を表示しています。

|項目|意味|
|:---|:---|
|Step|学習回数|
|epoch|学習データ全部を使った回数|
|ms|１回の学習にかかった平均時間|
|Minibatch loss|学習データを間引いた数|
|learning rate|学習をどのくらい慎重に進めるかのパラメータ|
|Minibatch error|学習データのエラー率|
|Validation error|検証データのエラー率|

最終的には、Vaildation errorを小さくすることを目標にしています。

プログラムのパラメータは次のようになっています。

|項目|設定|
|:---|:---|
|終了条件|エポック>10|
|バッチサイズ|64|
|活性化関数|ReLU|

しばらくの間は順調に学習が進んでいきますが、途中で学習速度が１０分の１にスローダウンしていきます。

```
Step 5000 (epoch 5.82), 434.0 ms
Step 5100 (epoch 5.93), 431.1 ms
Step 5200 (epoch 6.05), 430.0 ms
Step 5300 (epoch 6.17), 434.3 ms
Step 5400 (epoch 6.28), 533.1 ms
Step 5500 (epoch 6.40), 581.7 ms
Step 5600 (epoch 6.52), 581.4 ms
Step 5700 (epoch 6.63), 580.6 ms
Step 5800 (epoch 6.75), 582.4 ms
Step 5900 (epoch 6.87), 785.4 ms
Step 6000 (epoch 6.98), 975.2 ms
Step 6100 (epoch 7.10), 969.0 ms
Step 6200 (epoch 7.21), 2485.7 ms
Step 6300 (epoch 7.33), 4477.5 ms
Step 6400 (epoch 7.45), 4492.2 ms
Step 6500 (epoch 7.56), 3791.0 ms
Step 6600 (epoch 7.68), 4414.7 ms
Step 6700 (epoch 7.80), 4485.0 ms
Step 6800 (epoch 7.91), 4259.3 ms
Step 6900 (epoch 8.03), 3942.3 ms
```

モニタリングを見てみると、CPU使用率が最初は100%使っていましたが、途中から10%に制限されています。
![SlowRun_CPU使用率.png](https://qiita-image-store.s3.amazonaws.com/0/107056/daa7154b-49fe-56ab-675f-c83aa5ba8758.png)



t2.microインスタンスのメモリには余裕があります。

```
$ free -h
              total        used        free      shared  buff/cache   available
Mem:           990M        374M        372M        4.4M        243M        574M
Swap:            0B          0B          0B
```

# CPUクレジット
### CPUクレジットの制限事項
CPUクレジット残高を確認してみると最初にチャージされていた30カウントを使い切ってしまったことが分かります。

**CPUクレジット残高**
![SlowRun_CPUクレジット残高.png](https://qiita-image-store.s3.amazonaws.com/0/107056/2fe828ce-cea1-7513-adb8-be71da46f9f7.png)

**CPUクレジット使用状況**
![SlowRun_CPUクレジット使用状況.png](https://qiita-image-store.s3.amazonaws.com/0/107056/ca2f7c24-4fdd-a16a-0608-55492308c039.png)


さて、CPUクレジットの値は、何を意味しているのでしょうか。
実は、この値は、CPUを何分間100%使えるかの貯金を表しています。
なので、CPUクレジットがチャージされている間は、CPUクレジット使用状況のグラフを見ると、５分間に５カウントづつ消費しています。

t2.microで使っているCPUは「Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz」になります。

```
$ cat /proc/cpuinfo | grep "model name"
model name      : Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz
```

そしてt2.microは、CPU性能の10%を利用する権利を持っています。
10%をどのように割り当てられているかというと、１時間に６カウントのCPUクレジットが割り当てられます。１カウントで１分間CPUを100%利用する権利が得られるので、１時間（６０分）で６カウントというのがちょうど10%に相当します。

CPUクレジット残高は、最大２４時間分貯めることができます。t2.microであれば144カウント（6カウント/時間×24時間）まで貯めることができます。
また、t2.microインスタンスを作成した直後は、約30カウント割り当てられています。
インスタンスを停止させるとCPUクレジットは0にクリアされてしまいます。

t2.microでTensorFlowのLeNet-5 MINISTを実行し完走するには、CPUを100%使い続けて60分ほどかかります。
インスタンスを作成し、そのままTensorFlowのLeNet-5 MINISTを実行すると、30カウント分のCPUクレジットが不足していることになります。
停止させてCPUクレジットが0にクリアされてしまったt2.microでは、60カウント分のCPUクレジットが不足していることになります。

インスタンス作成直後、30カウント分足りないからといって、30カウントたまるのを５時間待ってから実行するのも、30カウント不足している状態でスローで走らせつづけるのも、どちらも約６時間かかり、終了時刻、インスタンス起動時間に差はありません。

### CPUクレジットを効率的に使う方法について検討してみる
CPUクレジットの制限事項を考慮したうえで、CPUを効率よく利用する方法について考え見ます。

1. **インスタンスを新規作成する**
CPUクレジットを稼ぐということだけを考えるのであれば、AMIにバックアップを取って毎回AMIをベースにインスタンスを作り直せば、スタート時の30カウント分が得です。
　

2. **スローダウン時間(15分)を利用する**
プログラムをあまり変えずにCPUカウントを稼ぐ方法として、15分間かけてスローダウンしていくことを利用することが考えられます。
CPU利用率が100%の状態でCPUクレジットが0になった場合、15分間かけてCPU利用率が10%になるように制限がかかっていきます。
CPUクレジットが０になった時点で、次に、CPUクレジットがある程度付与されるまでプログラムを一旦ストップさせると、CPUクレジットが０の状態で、15分間は10%以上のCPU性能を使うことができるので、少しは計算が進みます。
ところが、この方法には裏面があります。
まず、CPU利用率100%の期間が短いと、短時間で100%から10%へのスローダウンしてしまいます。
また、CPU利用率は、CPUクレジットがあっても、いきなり0%から100%にはならず、0%から100%になるには、6分程度かかってしまいます。
CPU利用率の制限は、定常的に100%だった状態から10%になるのは15分かかりますが、瞬間的に40%だったものが10%になるのは5分しかかかりません。
CPU利用率が100%になるためには、6分間分のCPUクレジットを貯める必要があり、60分間計算を停止させておく必要があります。
60分間、低速で計算を続ければCPU100%換算で6分間分の計算が進みますが、CPUを停止させてしまうと、この分の計算が進みません。
計算の停止と開始を繰り返す方法は、CPU利用率のスローダウンのスピードが速いのと、CPU利用率の立ち上がりにそれなりの時間がかかるため、「**あまり効果があるとは言えない**」ようです。
以下のようなスリープ処理を追加して試してみます。
    
    ```cpustop.py
        start_time = time.time()
        ．．．
        cpu_start_time = time.time() # ★開始時刻
        for step in xrange(int(num_epochs * train_size) // BATCH_SIZE):
            ．．．
            if step % EVAL_FREQUENCY == 0:
                elapsed_time = time.time() - start_time
                avrg_time = 1000 * elapsed_time / EVAL_FREQUENCY
                ．．．
                # ★平均計算時間が3000msec以上になったら50分間スリープする
                if avrg_time > 3000:
                    print("sleep t2.micro cpu. passed time=%d" % (time.time() - cpu_start_time))
                    time.sleep(3000)
                    print("run t2.micro cpu. passed time=%d" % (time.time() - cpu_start_time))
                
                start_time = time.time()
    ```
    グラフはこんな感じになります。10%づつスローに計算する代わりに、100%の計算を瞬間的に行っています。
    
    **CPU使用率**
    ![Sleep_CPU使用率.png](https://qiita-image-store.s3.amazonaws.com/0/107056/78343548-e5cd-1dcd-71cb-2fb1aa32bdcd.png)
    
    **CPUクレジット使用状況**
    ![Sleep_CPUクレジット使用状況.png](https://qiita-image-store.s3.amazonaws.com/0/107056/fdccaeba-1f11-d1f3-2d51-3dd33ef99a08.png)
    
    **CPUクレジット残高**
    ![Sleep_CPUクレジット残高.png](https://qiita-image-store.s3.amazonaws.com/0/107056/ff04e6b8-a4b3-2c5d-5276-e01b18dbd29b.png)
    　  

3. **複数台のインスタンスをリレーさせる**
システムとしては複雑になりますが、AWSのアラーム機能を利用してtc2.microを生成／削除しながら切り替えて、途中経過をS3に保存するような仕組みが効果がありそうです。ただ、そんなシステムを作るぐらいだったら、高性能なインスタンスをお金を払って借りたほうがよいという声も聞こえてきそうです。
    　
4. **事前にインスタンスを生成しておく**
t2.microは、24時間立ち上げて何もしなければ、CPUクレジットが144カウントまで蓄積されます。
144分以内に収まる計算であれば、前日にインスタンスを作成して放置しておくのも手です。
    　
5. **AWS Lambda**
リクエストを300秒以内に抑える必要がありますが、これがうまくできるようであれば、選択肢になりそうです。
EC2 t2.microと同じCPUでLambdaを実行するのであれば、500ステップ毎に区切って計算することができれば利用できそうです。
計算時間が約230秒、計算の途中経過の読み込みと保存に60秒、予備を10秒で割り当てる計算になります。
今回の計算は、メモリ512MBあればOKなので、Lambdaの無料枠で９日間の連続計算が可能です。
ところが、Lambdaの料金計算は、EC2とは考え方が異なります。
Lambdaの場合は、メモリが増えればCPUのスピードが向上していきます。
300秒ごとにスイッチする回数をできだけ減らしたいですね。そうすると、メモリは上限の1536MB使ったほうが、スイッチする回数が減ります。メモリを1536MB使った場合の１か月の無料枠は３日（266,667秒）になります。
AWS Lambdaのマシンスペックの調査結果はこちら（[リンク](http://qiita.com/olympic2020/items/6dedc6200100428d7aa0)）。

# AlexNet MNIST
続いては、AlexNet MNISTのベンチマークをEC2 t2.micro(無料利用枠対象)で実行し、GPUと比較してみます。
プログラムは、tensorflowのインストールディレクトリ配下の下記になります。

tensorflow/models/image/alexnet/alexnet_benchmark.py

プログラムのコメントによると、GPUではこのような性能がでるようです。

```
Forward pass:
Run on Tesla K40c: 145 +/- 1.5 ms / batch
Run on Titan X:     70 +/- 0.1 ms / batch

Forward-backward pass:
Run on Tesla K40c: 480 +/- 48 ms / batch
Run on Titan X:    244 +/- 30 ms / batch
```

一方、EC2 t2.microを実行したコンソールログはこのようになります。
EC2 t2.microがCPU 100%で動作していても、GPUとEC2 t2.microは約100倍の差があります。

```
conv1   [128, 56, 56, 64]
pool1   [128, 27, 27, 64]
conv2   [128, 27, 27, 192]
pool2   [128, 13, 13, 192]
conv3   [128, 13, 13, 384]
conv4   [128, 13, 13, 256]
conv5   [128, 13, 13, 256]
pool5   [128, 6, 6, 256]
2016-10-24 16:18:03.743222: step 10, duration = 9.735
2016-10-24 16:19:40.927811: step 20, duration = 9.675
2016-10-24 16:21:17.593104: step 30, duration = 9.664
2016-10-24 16:22:53.894240: step 40, duration = 9.684
2016-10-24 16:24:29.968737: step 50, duration = 9.597
2016-10-24 16:26:06.527066: step 60, duration = 9.686
2016-10-24 16:27:43.229298: step 70, duration = 9.689
2016-10-24 16:29:19.643403: step 80, duration = 9.679
2016-10-24 16:30:56.202710: step 90, duration = 9.588
2016-10-24 16:32:22.877673: Forward across 100 steps, 9.553 +/- 0.962 sec / batch
2016-10-24 16:42:27.229588: step 10, duration = 28.700
2016-10-24 16:49:33.216683: step 20, duration = 72.885
．．．
```

そして、Forward-backwardのstep 20あたりで、CPUクレジット30を使い切ります。
あとは徐々にスローダウンし、CPU 10%での動作になり、GPUの１０００分の１のスピードで延々と計算が続きます。
う～ん。全く勝負になりません。

# おわりに
ちょっと、視点を変えてみます。

AWS EC2 t2.micro(無料利用枠)でTensorFlowのLeNet-5 MNIST(convolutional.py)を実行して達成感を得るためにはどうしたらよういでしょうか。

インスタンス生成直後は、CPUクレジットが30あるわけですから、30～40分でそれなりに計算が終わるように設定してみます。

```変更版convolutional.py
NUM_EPOCHS = 6
```

そうです。変更は１か所だけです。学習回数をCPUクレジット内におさまるように設定してあげればOKです。

それでは、TensorFlowで、楽しいDeepLearning生活を！！