めもめも

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

(日本語版) 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.

Kernel PCA (Principal Component Analysis) の導出

何の話かというと

この本のKernel PCAの説明が相当に計算を端折っていて、これは読者もつらいだろぅ。。。と思って途中の計算を端折らずになるべく正確に導出してみました。

※実対称行列の固有値分解とLagrangeの未定乗数法は前提知識とします。

普通のPCA

はじめに、Kernelを使わない普通のPCAを導出します。

データセット \{{\mathbf x}_n\}_{n=1}^N,\,\,{\mathbf x}_n \in {\mathbf R}^{\rm D} の分散共分散行列を {\mathbf S} とします。この時、{\mathbf S} は正定値の実対称行列より、正(もしくは0)の固有値を持つ、互いに直行する固有ベクトルで固有値分解されます。この点に注意すると、第1主軸(その方向の成分の分散が最大になる方向)を {\mathbf u}_1\,\,(\|{\mathbf u}_1\|=1) とする時、これは、最大固有値を持つ固有ベクトルに一致することが次のように示されます。

次に、第2主軸({\mathbf u_1} に直行する方向で、その方向の成分の分散が最大になる方向){\mathbf u}_2\,\,(\|{\mathbf u}_2\|=1) は、2番目に大きな固有値を持つ固有ベクトル、さらに第3主軸({\mathbf u_1}{\mathbf u_2} の両方に直行する方向で、その方向の成分の分散が最大になる方向){\mathbf u}_3\,\,(\|{\mathbf u}_3\|=1) は、3番目に大きな固有値を持つ固有ベクトル・・・であることが帰納的に証明されます。


Kernel PCA

D次元空間のデータに対して、何らかの非線形関数 {\mathbf \phi}({\mathbf x}) \in {\mathbf R}^M\,\,(M>D) を用いて、M次元空間のデータに変換した後、データセット \{ {\mathbf \phi}({\mathbf x_n})  \}_{n=1}^N に対して、M次元空間上でのPCAを適用します。すると、次のような結論が得られます。(証明は後ほど掲載)

========
k({\mathbf x_n},{\mathbf x_{n'}})={\mathbf \phi}({\mathbf x}_n)^{\rm T}{\mathbf \phi}({\mathbf x}_{n'})\,\,(n,n'=1,\cdots,N) を成分とする対称行列を {\mathbf K}\in {\mathbf R}^N\times{\mathbf R}^N とする時、グラム行列 \tilde{\mathbf K} を次式で定義する。

 \tilde{\mathbf K} = {\mathbf K} - {\mathbf 1}_N{\mathbf K} - {\mathbf K}{\mathbf 1}_N + {\mathbf 1}_N{\mathbf K}{\mathbf 1}_N

ここに、{\mathbf 1}_N は、すべての成分が 1/N という値のN✕N行列である。

\tilde{\mathbf K} の正の固有値について、大きい方から並べたものを \lambda_1,\lambda_2,\cdots 、対応する(大きさ1の)固有ベクトルを {\mathbf u}_1,{\mathbf u}_2,\cdots \in {\mathbf R}^N とする時、第i主軸方向の単位ベクトルは、次式で与えられる。

 {\mathbf v}_i =\frac{1}{\sqrt{\lambda_i N}}  \sum_{n=1}^N u_{in}\left\{{\mathbf \phi}({\mathbf x}_n)-\frac{1}{N}\sum_{n'=1}^N{\mathbf \phi}({\mathbf x}_{n'})\right\}

この時、任意の点 {\mathbf x} について、非線形変換されたM次元空間における第i主軸成分は、次で計算される。

 {\mathbf \phi}({\mathbf x})^{\rm T}{\mathbf v}_i = \frac{1}{\sqrt{\lambda_i N}}  \sum_{n=1}^N u_{in}k({\mathbf x},{\mathbf x}_n)

ここに、k({\mathbf x},{\mathbf x}')={\mathbf \phi}({\mathbf x})^{\rm T}{\mathbf \phi}({\mathbf x}') をカーネル関数と呼ぶ。
========

この結果の重要なポイントして、{\mathbf u}_i の定義、および、最後の計算式を用いれば、非線形変換 {\mathbf \phi}({\mathbf x}) の具体的な形がわからなくても、カーネル関数 k({\mathbf x},{\mathbf x}') が与えられれば、第i主軸成分が計算できることが挙げられます。

たとえば、カーネル関数として、ガウスカーネル k({\mathbf x},{\mathbf x}_n)=\exp \left\{-\gamma(\| {\mathbf x}-{\mathbf x}_n \|^2)\right\} を用いた場合を考えます。これは、u_{in} が大きな値を取る n について、{\mathbf x_n} との距離が近いほど、その主軸成分が大きくなることを意味します。つまり、k近傍法のように「どのデータとの距離が近いかによって、主軸成分の大小が決まる」ことになります。これによって、元の {\mathbf x} 平面上でのデータの配置に関係なく、それぞれのデータとの距離を特徴量として抽出できることを意味します。(ガウスカーネルに対応するような {\mathbf \phi}({\mathbf x}) が存在することは別途証明が必要ですが、その証明はここでは省略します。)

それでは、前述の結論の証明です。





Kernel PCAの適用例

三日月形に並んだデータ群について、第1〜第4主軸成分の等高線を描きます。赤色は値が大きい所(山)で、青色は値が小さい所(谷)を示します。

gist.github.com

第1主軸成分を見ると、2つの三日月のどちらのデータに近いかで成分の大小が決まっていることが分かります。さらに第2主軸成分は、それぞれの三日月を左右に分割しています。高次の主軸成分になるほど、より細かくデータを分割することが観察できます。

「[改訂新版] プロのためのLinuxシステム構築・運用技術」が発売されます

www.amazon.co.jp

2010年12月に初版が発行された「プロのための Linuxシステム構築・運用技術」の改訂新版が発売されることになりました。2016年9月20日に販売開始予定です。改訂作業にあたりご協力いただいた皆様に改めてお礼を申し上げます。

本書の前付けより、「改訂にあたり」を掲載させていただきます。

改訂にあたり

 「はじめに」の日付にもある通り、本書の初版は2010年に出版されました。それから約6年の歳月を経て、改訂版を出版させていただくことになりました。対象とするLinuxディストリビューションは、Red Hat Enterprise Linux 5(RHEL5)から、Red Hat Enterprise Linux 7(RHEL7)へと変わり、各種のツールやコマンドは新しいものへと置き換わりました。しかしながら、6年前に書いた「はじめに」を読み返すと、驚くべきことに、その内容はまったく古くなってはいないようです。パブリッククラウドの活用が広がり、物理サーバーに一からOSをインストールする機会は減りました。コンテナ技術を活用して、OSの存在を意識することなく、手軽にアプリケーションをデプロイできるようになりました。Linuxサーバーを活用する上で必要となるOSの知識は、どんどん減っているかのようにも感じられます。その結果 ―― 思わぬ「落とし穴」にはまる機会が大きく増えました。
 誰でも手軽にLinuxサーバーが活用できるようになった今こそ、ストレージ、ネットワーク、そして、内部構造を含めた「OSの全体像」を把握することが、何よりも求められています。Linuxを専門とする方々はもちろんのこと、Linuxを基礎から勉強しようと思いながら、そのきっかけがなかった皆さんに、最新バージョンのRed Hat Enterprise Linuxに対応した本書をお届けしたいと思います。改訂版の執筆にあたり、「はじめに」に加えて、本書末の「おわりに」、そして、本文中のコラムについては、用語の表記をのぞいて、あえて初版の内容のままにしてあります。Unix/Linuxの世界で受けつがれる、今も変わらない「本質」の存在を感じていただければ幸いです。

おまけ

出版社よりいただいた、表紙カバーのデザインファイルです!