めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

(日本語版) Cloud ML Super Quick Tour

cloud.google.com

何の話かというと

先日、Google Cloud MLがベータ公開されました。超ざっくりまとめると、GCPのクラウド上で次のことができるようになります。

 (1) TensorFlowのコードを実行して学習済みモデルを作成する
 (2) 学習済みモデルをAPIサービスとして公開する(現在はアルファ版)

(1)については、ハイパーパラメーターの自動チューニングや分散学習処理なども利用できるのですが、ここでは、単純に、既存のTensorFlowのコードをCloud MLに載せるための最低限の手順を説明します。

例として、下記のサンプルコードを使用します。全結合層のみの単層ニューラルネットワークでMNISTデータセットを分類する簡単な例です。

MNIST single layer network.ipynb

ローカルで実行する場合からの修正点

まず、コードの実行に必要なファイルを1つのディレクトリーにまとめて、ライブラリー化します。実行ファイルが「task.py」の1つであれば、次のような構成になります。ディレクトリー名などは任意です。

trainer/
├── __init__.py   # 空ファイル
└── task.py       # 実行ファイル

そして、実行ファイルの末尾に次のコードを挿入します。最後の run メソッドにより、main() 関数の実行が開始されます。

if __name__ == '__main__':
    tf.app.run()

外部とのファイルのやり取りは、Cloud Storageを経由する必要があります。これは、コード内のファイルパスをCloud StorageのURI「gs://...」に書き換えればOKです。ただし、ローカルでテストした上でCloud MLに投げる場合を考えて、実行時のコマンドラインオプションでパスを指定できるようにするとよいでしょう。具体的には、次のようなディレクトリーが考えられます。

・学習中のチェックポイントファイルを出力するディレクトリー
・学習済みモデルのバイナリーをファイルとして出力するディレクトリー(ファイル名は「export」に固定)
・TensorBoard用のログデータを出力するディレクトリー
・トレーニング用のデータを読み込むディレクトリー

(トレーニング用のデータについては、Cloud Storageの利用が必須というわけではありません。Cloud Dataflowなど、他のサービスからのデータを入力ソースにする方法も用意されるようです。)

今回の例であれば、実行ファイル task.py の末尾を次のようにしておきます。オプション「--train_dir」と「--model_dir」でチェックポイントファイルとモデルファイルを出力するディレクトリーを指定します。ついでに「--train_step」で学習処理のループ数も指定できるようにしています。トレーニング用のデータは、TensorFlowのライブラリー機能で、インターネット上のデータを直接に取得します。

def main(_):
    parser = argparse.ArgumentParser()
    parser.add_argument('--train_dir', type=str, default='/tmp/train')  # Checkpoint file
    parser.add_argument('--model_dir', type=str, default='/tmp/model')  # Model file
    parser.add_argument('--train_step', type=int, default=2000)         # Training steps
    args, _ = parser.parse_known_args()
    run_training(args)

if __name__ == '__main__':
    tf.app.run()

そして、ここが一番特殊なのですが、学習済みモデルをAPIサービス化するために、APIの入出力となる要素をTensorFlowのCollectionオブジェクトで指定します。Collectionオブジェクトは、Key-Valueスタイルで任意のオブジェクトを格納するものですが、特に「inputs」というキーで入力を受け付けるPlaceholder一式、「outputs」というキーでAPIから返却する値の一式を指定します。今の例であれば、次のようになります。

input_key = tf.placeholder(tf.int64, [None,])
x = tf.placeholder(tf.float32, [None, 784])

inputs = {'key': input_key.name, 'image': x.name}
tf.add_to_collection('inputs', json.dumps(inputs))

p = tf.nn.softmax(tf.matmul(hidden1, w0) + b0)
output_key = tf.identity(input_key)

outputs = {'key': output_key.name, 'scores': p.name}
tf.add_to_collection('outputs', json.dumps(outputs))

「inputs」「outputs」共に、指定するオブジェクト(のname属性)をディクショナリーにまとめた上で、それをJSONにシリアライズしたものをCollectionオブジェクトに突っ込んでおきます。ディクショナリーのキーは、APIでやり取りする際の名前になります。この例では、入力画像「x」と予測結果(確率のリスト)「p」の他に、入力値をそのまま出力する「input_key」と「output_key」を入出力要素に加えています。これは複数のデータをまとめてAPIに投げた時に、返ってきた結果のそれぞれが、どの入力データに対応するものかを区別するために加えています。

以上を考慮して修正したコードがこちらになります。

task.py

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
import argparse, os, json
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

def run_training(args):
    # Define filepath for checkpoint and final model
    checkpoint_path = os.path.join(args.train_dir, 'checkpoint')
    model_path = os.path.join(args.model_dir, 'export') # Filename should be 'export'.
    num_units = 1024
    
    x = tf.placeholder(tf.float32, [None, 784])
    
    w1 = tf.Variable(tf.truncated_normal([784, num_units]))
    b1 = tf.Variable(tf.zeros([num_units]))
    hidden1 = tf.nn.relu(tf.matmul(x, w1) + b1)
    
    w0 = tf.Variable(tf.zeros([num_units, 10]))
    b0 = tf.Variable(tf.zeros([10]))
    p = tf.nn.softmax(tf.matmul(hidden1, w0) + b0)
    
    t = tf.placeholder(tf.float32, [None, 10])
    loss = -tf.reduce_sum(t * tf.log(p))
    train_step = tf.train.AdamOptimizer().minimize(loss)
    correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    # Define key element
    input_key = tf.placeholder(tf.int64, [None,], name='key')
    output_key = tf.identity(input_key)

    # Define API inputs/outpus object
    inputs = {'key': input_key.name, 'image': x.name}
    outputs = {'key': output_key.name, 'scores': p.name}
    tf.add_to_collection('inputs', json.dumps(inputs))
    tf.add_to_collection('outputs', json.dumps(outputs))
    
    saver = tf.train.Saver()
    sess = tf.InteractiveSession()
    sess.run(tf.initialize_all_variables())

    i = 0
    for _ in range(args.train_step):
        i += 1
        batch_xs, batch_ts = mnist.train.next_batch(100)
        sess.run(train_step, feed_dict={x: batch_xs, t: batch_ts})
        if i % 100 == 0:
            loss_val, acc_val = sess.run([loss, accuracy],
                feed_dict={x:mnist.test.images, t: mnist.test.labels})
            print ('Step: %d, Loss: %f, Accuracy: %f'
                   % (i, loss_val, acc_val))
            saver.save(sess, checkpoint_path, global_step=i)

    # Export the final model.
    saver.save(sess, model_path)


def main(_):
    parser = argparse.ArgumentParser()
    parser.add_argument('--train_dir', type=str, default='/tmp/train')  # Checkpoint directory
    parser.add_argument('--model_dir', type=str, default='/tmp/model')  # Model directory
    parser.add_argument('--train_step', type=int, default=2000)         # Training steps
    args, _ = parser.parse_known_args()
    run_training(args)


if __name__ == '__main__':
    tf.app.run()

Cloud MLの環境準備

それでは、Cloud MLの環境を用意して、先のコードを実行します。Cloud MLにジョブを投げるには、Cloud ML用のSDKを導入したローカル環境が必要となりますが、ここでは、Cloud Shellから実行することにします。Cloud Shell以外の環境を準備する方法は、こちらを参照してください。

新規プロジェクトを作成して、API ManagerからCloud MLのAPIを有効化したら、Cloud Shellを起動してSDKを導入します。

$ curl https://storage.googleapis.com/cloud-ml/scripts/setup_cloud_shell.sh | bash
$ export PATH=${HOME}/.local/bin:${PATH}
$ curl https://storage.googleapis.com/cloud-ml/scripts/check_environment.py | python
Success! Your environment is configured correctly.

サービスアカウントからジョブを投げるため、次のコマンドでサービスアカウントに対してプロジェクトの「編集者」権限を付与します。

$ gcloud beta ml init-project

先程のコードをホームディレクトリーの下の「trainer」ディレクトリー以下にに用意します。

$HOME/trainer/
├── __init__.py   # 空ファイル
└── task.py       # 実行ファイル

まずは、ローカルで実行してみます。テスト実行なので、ループ数は少なめにします。

$ mkdir -p /tmp/train /tmp/model
$ cd $HOME
$ python -m trainer.task --train_step=200
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-labels-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-labels-idx1-ubyte.gz
Step: 100, Loss: 3183.995850, Accuracy: 0.903500
Step: 200, Loss: 2237.709229, Accuracy: 0.934500

$ ls -l /tmp/train /tmp/model/
/tmp/model/:
total 9584
-rw-r--r-- 1 enakai enakai     203 Oct  5 17:14 checkpoint
-rw-r--r-- 1 enakai enakai 9770436 Oct  5 17:14 export
-rw-r--r-- 1 enakai enakai   35514 Oct  5 17:14 export.meta
/tmp/train:
total 28744
-rw-r--r-- 1 enakai enakai     163 Oct  5 17:14 checkpoint
-rw-r--r-- 1 enakai enakai 9770436 Oct  5 17:14 checkpoint-100
-rw-r--r-- 1 enakai enakai   35514 Oct  5 17:14 checkpoint-100.meta
-rw-r--r-- 1 enakai enakai 9770436 Oct  5 17:14 checkpoint-200
-rw-r--r-- 1 enakai enakai   35514 Oct  5 17:14 checkpoint-200.meta

Cloud MLによるモデルの学習

クラウド上で学習処理を実行します。はじめに、データ保存用のバケットを作成しておきます。バケット名は任意ですが、お作法としてプロジェクト名を含めておくと良いでしょう。

$ PROJECT_ID=project01 # your project ID
$ TRAIN_BUCKET="gs://$PROJECT_ID-mldata"
$ gsutil mkdir $TRAIN_BUCKET

ジョブ名を決めて、Cloud MLのAPIにジョブを投げつけます。「--staging-bucket」オプションで指定したバケットの下に「cloudmldist」フォルダーが作成されて、その中にコード一式が転送されて、処理が開始します。チェックポイントファイルとモデルファイルを出力するフォルダーを事前にgsutilコマンドで作成している点に注意してください。(gsutilコマンドは空のフォルダーを作ることができないので、ダミーファイルをフォルダー内にコピーしています。本当は、コードの中で自動作成するようにした方が便利ですが、出力先フォルダーが必要な点を強調するために手動作成しています。)

$ JOB_NAME="job01"
$ touch .dummy
$ gsutil cp .dummy $TRAIN_BUCKET/$JOB_NAME/train/
$ gsutil cp .dummy $TRAIN_BUCKET/$JOB_NAME/model/
$ gcloud beta ml jobs submit training $JOB_NAME \
  --region=us-central1 \
  --package-path=trainer --module-name=trainer.task \
  --staging-bucket=$TRAIN_BUCKET \
  -- \
  --train_dir="$TRAIN_BUCKET/$JOB_NAME/train" \
  --model_dir="$TRAIN_BUCKET/$JOB_NAME/model"

createTime: '2016-10-05T08:53:35Z'
jobId: job01
state: QUEUED
trainingInput:
  args:
  - --train_dir=gs://project01/job01/train
  - --model_dir=gs://project01/job01/model
  packageUris:
  - gs://project01/cloudmldist/1475657612/trainer-0.0.0.tar.gz
  pythonModule: trainer.task
  region: us-central1

次のコマンドでジョブの実行を見守ります。最後に「state: SUCCEEDED」になれば完了です。

$ watch -n1 gcloud beta ml jobs describe --project $PROJECT_ID $JOB_NAME
createTime: '2016-10-05T08:53:35Z'
jobId: job01
startTime: '2016-10-05T08:53:45Z'
state: RUNNING
trainingInput:
  args:
  - --train_dir=gs://project01/job01/train
  - --model_dir=gs://project01/job01/model
  packageUris:
  - gs://project01/cloudmldist/1475657612/trainer-0.0.0.tar.gz
  pythonModule: trainer.task
  region: us-central1

実行中のログは、Stackdriverのログ管理画面から「Cloud ML」のログを選択して確認することができます。

正常終了した場合は、次のようにモデルファイル「export」が作成されています。

$ gsutil ls $TRAIN_BUCKET/$JOB_NAME/model/export*
gs://project01/job01/model/export
gs://project01/job01/model/export.meta

学習済みモデルのサービス化

学習済みモデルファイル「export」を利用して、APIサービスを立ち上げます。モデル名を指定して、次のコマンドを実行します。複数のモデルファイルをバージョン管理することが可能で、ここでは、バージョン名「v1」で立ち上げた上で、これをデフォルトサービスに指定しています。

$ MODEL_NAME="MNIST"
$ gcloud beta ml models create $MODEL_NAME
$ gcloud beta ml versions create \
  --origin=$TRAIN_BUCKET/$JOB_NAME/model --model=$MODEL_NAME v1
$ gcloud beta ml versions set-default --model=$MODEL_NAME v1

サービスが起動するまで、1〜2分かかるので、その間に、次のPythonスクリプトを実行して、テスト用データ「data.json」を作成します。1行に1つのイメージデータとkey番号が含まれるJSONファイルです。

import json
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
with open("data.json", "w") as file:
    for i in range(10):
        data = {"image": mnist.test.images[i].tolist(), "key": i}
        file.write(json.dumps(data)+'\n')

gcloudコマンドでAPIにデータを投げつけると、それぞれのデータに対する予測結果が返って来ます。

$ gcloud beta ml predict --model=${MODEL_NAME} --json-instances=data.json
predictions:
- key: 0
  scores:
  - 2.53733e-08
  - 6.47722e-09
  - 2.23573e-06
  - 5.32844e-05
  - 3.08012e-10
  - 1.33022e-09
  - 1.55983e-11
  - 0.99991
  - 4.39428e-07
  - 3.38841e-05
- key: 1
  scores:
  - 1.98303e-08
  - 2.84799e-07
  - 0.999985
  - 1.47131e-05
  - 1.45546e-13
  - 1.90945e-09
  - 3.50033e-09
  - 2.24941e-18
  - 2.60025e-07
  - 1.45738e-14
- key: 2
  scores:
  - 3.63027e-09
...

REST APIに直接にアクセスする際のURLは、こちらを参照してください。

分散学習について

現状では、分散学習を意識したTensorFlowのコードを用意する必要があります。

・参考:Distributed TensorFlow

今後、分散学習用のコードをもう少し簡単に書けるようにするライブラリーなどが登場するものと期待されます。

分散学習用のコードを今すぐ書いてみたい!という方は下記を参考にしてください。

enakai00.hatenablog.com

Disclaimer: All code snippets are released under Apache 2.0 License. This is not an official Google product.