Kubernetesドリブンの機械学習プラットフォーム「Seldon」を触ってみる

# はじめに

## Seldonとは

2015年12月にOSSとして公開された、イギリス発のオープンソースの機械学習プラットフォームです。

**「Kubernetesをアーキテクチャの中心に据えている」**のが特徴で、**「Kafka + Spark + Zookeeper(機械学習)」**や**「Grafana + Influxdb + Fluentd(ログ可視化)」**といった複雑なシステム構成からなる機械学習プラットフォームを、Kuberetesを使うことで、非常に簡単に立ち上げることができます。

また、**「学習済モデルをコンテナに押し込んでmicroservice化する仕組み」** や **「バッチ系の学習処理(luigiベース)をDockerfileに押し込んでKubernetesのjobとして投げる仕組み」**など、コンテナ構成の利点を最大限に活かした設計が取られています。

少人数ながらも精力的なリリースが続けられており、非常に面白いプロダクトだと感じたので、今回ご紹介させていただきました。興味を持っていただければ幸いです。



## 類似ソフトウェアとの比較

「OSSの機械学習プラットフォーム」として、類似点が多い[PredictionIO](https://predictionio.incubator.apache.org/)と比較してみました。

| | PredictionIO | Seldon |
| ----- | ----- | ----- |
| ライセンス| **Apache License 2.0** | **Apache License 2.0** | 
| メインになる開発言語 | **Scala** | **Python** | 
| 対応する機械学習領域 | **汎用(Template Engine)** | Recommendation / Prediction | 
| 標準サポートモデル | MLlib   | **TensorFlow, Keras, Vowpal Wabbit, XGBoost, Gensim** |
| コンテナ対応 | 非公式 | **Kubernetes** | 
| 1st release | **2013/02** | 2015/12 |
| GitHubのStar数(2017/06/29時点) | **10,252**  | 1,078 | 
| 公式ドキュメント | **(それなりに)充実**  | 必要最低限 | 
| その他 | **日本語コミュニティがある**  | 英語/日本語を問わず情報が少ない | 

こうして比較すると「うーん、ちょっとなー…」という感じですが、実際に触ってみるとシンプルなしくみでわかりやすく、意外となんとかなりそうでした。

「PredictioIOの仕組みは複雑すぎる…」と感じた方や、「基本的にはPythonベースでやりたい」という方は、是非一度触ってみて頂きたいと思います。 ~~知見増えろの意~~

# Seldonを触ってみる

というわけで、動作確認を行いました。

* 自作PC (i7-4790 @ 3.60GHz, SSD) (メモリ 32GB)
    * Windows 10 Pro 64bit
    * Hyper-V
* コンソール環境
    * Cygwin
        * Python 2.7.13
        * make, htpasswd(httpd-tools), (jq)
* Kubernetes
    * [Minikube](https://github.com/kubernetes/minikube) v0.20.0
    * Kubectl v1.6.3

ドキュメントには**[「minicubeで動かす場合最低12GBのメインメモリが必要」](https://github.com/SeldonIO/seldonio.github.com/blob/master/install.md#troubleshooting)**[^1] と書かれており、「16GBのMBPだと余裕ないかな…？」とWindows環境で試したのですが、minikubeやkubectl周りでのトラブルに見舞われがちでした。
素直にMacOSやLinux系を使うか、[GCPを使うやり方](http://docs.seldon.io/kubernetes-google-cloud.html)をオススメします。

それでも「Windows環境で試したい」という方は、後述する「※Windows + Cygwinの場合」をあわせてご覧ください。

[^1]: なお、いろいろ頑張ってもメモリ6GBは必要になるみたいです。豪快ですね…


## Seldonの起動

まずKubernetesクラスタを起動します。
今回は[公式ドキュメント](https://github.com/SeldonIO/seldonio.github.com/blob/master/install.md#troubleshooting)で指定されている通りにメモリは12GB[^2]とし、ディスクサイズもざっくり多めに割り当ててました[^3]。ドライバはHyper-Vを指定しています。

```
c:\>minikube start --vm-driver="hyperv" --memory=12000 --disk-size=40g
Starting local Kubernetes v1.6.4 cluster...
Starting VM...
Downloading Minikube ISO
 90.95 MB / 90.95 MB [==============================================] 100.00% 0s
Moving files into cluster...
Setting up certs...
Starting cluster components...
Connecting to cluster...
Setting up kubeconfig...
Kubectl is now configured to use the cluster.
```

クラスタが正しく起動しているのを確認。

[^2]: 12GBが`--memory=12288`でないのが気になってしまう真面目な方はきちんと指定してあげてください。
[^3]: デフォルト値である20GBでは、試行錯誤していたらDisk Fullになってしまいました。[hostpath.json](https://github.com/SeldonIO/seldon-server/blob/v1.4.6/kubernetes/conf/hostpath.json#L12)には「50Gi」と指定されているので、それくらい用意してあげた方がよいのかもしれません。

```
c:\>kubectl cluster-info
Kubernetes master is running at https://192.168.11.224:8443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
```

Seldonの最新リリースバージョンを[こちら](https://github.com/SeldonIO/seldon-server/releases)から確認し、ソースコードを取得します。今回はv1.4.6を利用し、`C:\seldon-server`として配置しました。

```
$ cd /cygdrive/c/
$ git clone https://github.com/seldonio/seldon-server
$ cd seldon-server/
$ git checkout -b v1.4.6 refs/tags/v1.4.6
```

Kubernetes Configurationの作成を行います。make, htpasswdコマンドが必要になるので、事前に用意しておいてください。
なお、[公式インストールマニュアル](https://github.com/SeldonIO/seldonio.github.com/blob/master/install.md#configure)にはパラメータの変更例が記述されていますが、Minikubeで動かす場合はデフォルトのままで問題ありません。

```
$ cd /cygdrive/c/seldon-server/kubernetes/conf
$ make clean conf
rm -f /home/teru/seldon-server/build/build_versions_generated
rm -f mysql.json
rm -f memcache.json
rm -f control.json
rm -f td-agent-server.json
rm -f spark-master.json
rm -f spark-workers.json
rm -f server.json
rm -f influxdb-grafana.json
rm -f examples/reuters/import-data-job.json
rm -f examples/ml100k/ml100k-import.json
rm -f examples/ml10m/ml10m-import-item-similarity.json
rm -f examples/ml10m/ml10m-import-matrix-factorization.json
rm -f examples/finefoods/train-finefoods.json
rm -f examples/tensorflow_deep_mnist/train-tensorflow-deep-mnist.json
rm -f examples/tensorflow_deep_mnist/load-model-tensorflow-deep-mnist.json
rm -f microservice-.json
rm -f glusterfs.json
rm -f zookeeper.json
rm -f kafka.json
rm -f dev/server.json
rm -f dev/control.json
rm -f dev/iago.json
rm -f dev/locust-slave.json.template
rm -f dev/locust-master.json.template
rm -f analytics/*.json
rm -f microservice_pipeline.rest.template
rm -f microservice_pipeline.rpc.template
rm -f models/stream-itemsim-create.json
rm -f models/stream-itemsim-dbupload.json
rm -f rpc/create-proto-jar-job.template.json
rm -f mysql-google-cloudsql.json
rm -f spark-ui.json
rm -f proxy-server.json
rm -f spark.htpasswd
kubectl delete secret grafana-admin-password  > /dev/null 2>&1 || : && \
kubectl create secret generic grafana-admin-password --from-literal=grafana-admin-password.txt="admin"
secret "grafana-admin-password" created
htpasswd -bc spark.htpasswd spark spark
Adding password for user spark
kubectl delete secret sparkui-secret  > /dev/null 2>&1 || : && \
kubectl create secret generic sparkui-secret --from-file=./spark.htpasswd
secret "sparkui-secret" created
created control.json
created mysql.json
created memcache.json
created td-agent-server.json
created spark-master.json
created spark-workers.json
created examples/reuters/import-data-job.json
created glusterfs.json
created server.json
created examples/ml100k/ml100k-import.json
created dev/server.json
created dev/control.json
created influxdb-grafana.json
created zookeeper.json
created kafka.json
created examples/finefoods/train-finefoods.json
created examples/tensorflow_deep_mnist/train-tensorflow-deep-mnist.json
created examples/tensorflow_deep_mnist/load-model-tensorflow-deep-mnist.json
created microservice_pipeline.rest.template
created microservice_pipeline.rpc.template
created examples/US_stocks/train-US-stocks.json
created mysql-google-cloudsql.json
created spark-ui.json
created proxy-server.json
```

`seldon-server/kubernetes/bin`にパスを通し、`seldon-up`スクリプトで起動します。[^4]
私の環境では起動完了まで5～10分くらいかかりました。気長に待ちましょう。

[^4]: パスは通さなくても動きますが、`start-microservice`など一部パスが通ってる前提のスクリプトも存在するので、通しておいたほうが無難です。

```
$ ls /cygdrive/c/seldon-server/kubernetes/bin
create-proto-jar  launch-locust-load-test  seldon-cli  seldon-down  seldon-up  start-microservice

$ export PATH="$PATH:/cygdrive/c/seldon-server/kubernetes/bin"

$ seldon-up
Starting seldon version [1.4.6]
Creating hostpath persistent volume
persistentvolume "host-volume" created
persistentvolumeclaim "seldon-claim" created
Starting core servces
deployment "mysql" created
service "mysql" created
deployment "memcached1" created
service "memcached1" created
deployment "memcached2" created
service "memcached2" created
deployment "redis" created
service "redis" created
service "zookeeper-1" created
service "zookeeper-2" created
service "zookeeper-3" created
deployment "zookeeper1" created
deployment "zookeeper2" created
deployment "zookeeper3" created
deployment "seldon-control" created
deployment "influxdb-grafana" created
service "monitoring-influxdb" created
service "monitoring-grafana" created
NAME                               READY     STATUS              RESTARTS   AGE
influxdb-grafana-842592602-h0m2t   0/2       Pending             0          1s
memcached1-2136693305-lzmmj        0/1       ContainerCreating   0          3s
memcached2-2533120572-9wckh        0/1       ContainerCreating   0          3s
mysql-2529449154-21g6z             0/1       ContainerCreating   0          4s
redis-1963070708-sp2h1             0/1       ContainerCreating   0          3s
seldon-control-2582542290-dl869    0/1       ContainerCreating   0          1s
zookeeper1-467704625-8crjx         0/1       ContainerCreating   0          2s
zookeeper2-1006738229-rqhkt        0/1       ContainerCreating   0          2s
zookeeper3-1545771833-dwr97        0/1       ContainerCreating   0          2s
Waiting for pods to be running as found 9 in non-running state
Sleeping for 5 seconds...
NAME                               READY     STATUS              RESTARTS   AGE
influxdb-grafana-842592602-h0m2t   0/2       ContainerCreating   0          7s
memcached1-2136693305-lzmmj        0/1       ContainerCreating   0          9s
memcached2-2533120572-9wckh        0/1       ContainerCreating   0          9s
mysql-2529449154-21g6z             0/1       ContainerCreating   0          10s
redis-1963070708-sp2h1             0/1       ContainerCreating   0          9s
seldon-control-2582542290-dl869    0/1       ContainerCreating   0          7s
zookeeper1-467704625-8crjx         0/1       ContainerCreating   0          8s
zookeeper2-1006738229-rqhkt        0/1       ContainerCreating   0          8s
zookeeper3-1545771833-dwr97        0/1       ContainerCreating   0          8s
Waiting for pods to be running as found 9 in non-running state
Sleeping for 5 seconds...

... (中略)

Waiting for pods to be running as found 1 in non-running state
Sleeping for 3 seconds...
deployment "spark-worker-controller" created
deployment "spark-ui-proxy-controller" created
service "spark-ui-proxy" created
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Setting up memcached
Writing data to file[/seldon-data/conf/zkroot/config/memcached/_data_]
Writing data to file[/seldon-data/conf/zkroot/config/memcached/_data_]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
updated zk node[/config/memcached]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Writing data to file[/seldon-data/conf/zkroot/config/dbcp/_data_]
Setting up Databases
Writing data to file[/seldon-data/conf/zkroot/config/dbcp/_data_]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
updated zk node[/config/dbcp]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Trying to create the client
Adding api DB to MySQL DB 'ClientDB'
Adding JS consumer key for client 'test' : 'CX3W4JJIWFMGJ6E30F6T'
Adding REST API key for client 'test' : consumer_key='EZ137JB4KP1MZFBDXWHY' consumer_secret='MTPIBH760LE2XOJBB50W'
Writing data to file[/seldon-data/conf/zkroot/all_clients/test/_data_]
updated zk node[/all_clients/test]
Adding grafana dashboard, response code 200
Starting Seldon API server
deployment "seldon-server" created
service "seldon-server" created
Defaulting container name to influxdb.
Use 'kubectl describe pod/influxdb-grafana-842592602-h0m2t' to see all of the containers in this pod.
deployment "kafka-stream-impressions" created
deployment "kafka-stream-predictions" created
```

30分以上かかっても完了しない場合は異常終了している可能性があるので、`kubectl get all`や`kubectl get events`といったコマンドで状態を確認してみてください。以下に正常終了後の`kubectl get all`の結果を示します。

```
$ kubectl get all
NAME                                            READY     STATUS    RESTARTS   AGE
po/influxdb-grafana-842592602-n9xl6             2/2       Running   0          12m
po/kafka-controller-1424591021-q76xh            1/1       Running   0          5m
po/kafka-stream-impressions-169212079-4s9m7     1/1       Running   0          3m
po/kafka-stream-predictions-140764527-vqqs4     1/1       Running   0          3m
po/memcached1-2136693305-lzg9l                  1/1       Running   0          12m
po/memcached2-2533120572-djxbh                  1/1       Running   0          12m
po/mysql-2529449154-v0j1d                       1/1       Running   0          12m
po/redis-1963070708-qtqk9                       1/1       Running   0          12m
po/seldon-control-2582542290-rn0q2              1/1       Running   0          12m
po/seldon-server-3173692685-sxpcr               3/3       Running   0          3m
po/spark-master-controller-3720462731-pr84p     1/1       Running   0          4m
po/spark-ui-proxy-controller-1688034969-vrtwj   2/2       Running   0          3m
po/spark-worker-controller-3381690000-62g1s     1/1       Running   0          3m
po/spark-worker-controller-3381690000-6dkpz     1/1       Running   0          3m
po/td-agent-server-3988194731-xll8z             1/1       Running   0          5m
po/zookeeper1-467704625-2smxw                   1/1       Running   0          12m
po/zookeeper2-1006738229-870qd                  1/1       Running   0          12m
po/zookeeper3-1545771833-lrwxh                  1/1       Running   0          12m

NAME                      CLUSTER-IP   EXTERNAL-IP   PORT(S)                       AGE
svc/kafka-service         10.0.0.167   <nodes>       9092:30010/TCP                5m
svc/kubernetes            10.0.0.1     <none>        443/TCP                       13m
svc/memcached1            10.0.0.237   <none>        11211/TCP                     12m
svc/memcached2            10.0.0.103   <none>        11211/TCP                     12m
svc/monitoring-grafana    10.0.0.185   <pending>     80:30002/TCP                  12m
svc/monitoring-influxdb   10.0.0.198   <none>        8083/TCP,8086/TCP             12m
svc/mysql                 10.0.0.253   <none>        3306/TCP                      12m
svc/redis                 10.0.0.192   <none>        6379/TCP                      12m
svc/seldon-server         10.0.0.221   <nodes>       80:30015/TCP,5000:30017/TCP   3m
svc/spark-master          10.0.0.211   <none>        7077/TCP                      4m
svc/spark-ui-proxy        10.0.0.145   <pending>     8000:30005/TCP                3m
svc/spark-webui           10.0.0.162   <none>        8080/TCP                      4m
svc/td-agent-server       10.0.0.222   <none>        24224/TCP,24224/UDP           5m
svc/zookeeper-1           10.0.0.219   <none>        2181/TCP,2888/TCP,3888/TCP    12m
svc/zookeeper-2           10.0.0.225   <none>        2181/TCP,2888/TCP,3888/TCP    12m
svc/zookeeper-3           10.0.0.151   <none>        2181/TCP,2888/TCP,3888/TCP    12m

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/influxdb-grafana            1         1         1            1           12m
deploy/kafka-controller            1         1         1            1           5m
deploy/kafka-stream-impressions    1         1         1            1           3m
deploy/kafka-stream-predictions    1         1         1            1           3m
deploy/memcached1                  1         1         1            1           12m
deploy/memcached2                  1         1         1            1           12m
deploy/mysql                       1         1         1            1           12m
deploy/redis                       1         1         1            1           12m
deploy/seldon-control              1         1         1            1           12m
deploy/seldon-server               1         1         1            1           3m
deploy/spark-master-controller     1         1         1            1           4m
deploy/spark-ui-proxy-controller   1         1         1            1           3m
deploy/spark-worker-controller     2         2         2            2           3m
deploy/td-agent-server             1         1         1            1           5m
deploy/zookeeper1                  1         1         1            1           12m
deploy/zookeeper2                  1         1         1            1           12m
deploy/zookeeper3                  1         1         1            1           12m

NAME                                      DESIRED   CURRENT   READY     AGE
rs/influxdb-grafana-842592602             1         1         1         12m
rs/kafka-controller-1424591021            1         1         1         5m
rs/kafka-stream-impressions-169212079     1         1         1         3m
rs/kafka-stream-predictions-140764527     1         1         1         3m
rs/memcached1-2136693305                  1         1         1         12m
rs/memcached2-2533120572                  1         1         1         12m
rs/mysql-2529449154                       1         1         1         12m
rs/redis-1963070708                       1         1         1         12m
rs/seldon-control-2582542290              1         1         1         12m
rs/seldon-server-3173692685               1         1         1         3m
rs/spark-master-controller-3720462731     1         1         1         4m
rs/spark-ui-proxy-controller-1688034969   1         1         1         3m
rs/spark-worker-controller-3381690000     2         2         2         3m
rs/td-agent-server-3988194731             1         1         1         5m
rs/zookeeper1-467704625                   1         1         1         12m
rs/zookeeper2-1006738229                  1         1         1         12m
rs/zookeeper3-1545771833                  1         1         1         12m
```

# 動作確認

無事起動できたら、サンプルを使って動作確認をしてみましょう。

## Movielens 100K Sample

機械学習で定番のMovielens(100Kレコード)データセットを使ったレコメンドです。

* [Content Recommendation Guide \| Seldon Documentation](http://docs.seldon.io/ml100k.html)

データを読み込み、学習モデルを作成します。

```
$ cd /cygdrive/c/seldon-server/kubernetes/conf/examples/ml100k

$ kubectl create -f ml100k-import.json
job "ml100k-import" created
```

`kubectl get jobs`コマンドを叩き、ジョブが完了するまで（「DESIRED」の値と 「SUCCESSFUL」の値が同じになるまで）待ちます。

```
$ kubectl get jobs -l name=ml100k-import
NAME            DESIRED   SUCCESSFUL   AGE
ml100k-import   1         0            23s

(2～3分後)

$ kubectl get jobs -l name=ml100k-import
NAME            DESIRED   SUCCESSFUL   AGE
ml100k-import   1         1            2m
```

学習モデルの作成が完了したら、レコメンドを行います。

```
$ seldon-cli api --client-name ml100k --endpoint /js/recommendations --item 50 --limit 4
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
response code 200
{"size":4,"requested":4,"list":[{"id":"181","name":"","type":1,"first_action":1498640323000,"last_action":1498640323000,"popular":false,"demographics":[],"attributes":{},"attributesName":{"recommendationUuid":"1","release":"14-Mar-1997","title":"Return of the Jedi (1983)","url":"http://us.imdb.com/M/title-exact?Return%20of%20the%20Jedi%20(1983)"}},{"id":"127","name":"","type":1,"first_action":149864
0323000,"last_action":1498640323000,"popular":false,"demographics":[],"attributes":{},"attributesName":{"recommendationUuid":"1","release":"01-Jan-1972","title":"Godfather, The (1972)","url":"http://us.imdb.com/M/title-exact?Godfather,%20The%20(1972)"}},{"id":"1","nam
e":"","type":1,"first_action":1498640323000,"last_action":1498640323000,"popular":false,"demographics":[],"attributes":{},"attributesName":{"recommendationUuid":"1","release":"01-Jan-1995","title":"Toy Story (1995)","url":"http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)"}},{"id":"100","name":"","type":1,"first_action":1498640323000,"last_action":1498640323000,"popular":false,"demographics":[],
"attributes":{},"attributesName":{"recommendationUuid":"1","release":"14-Feb-1997","title":"Fargo (1996)","url":"http://us.imdb.com/M/title-exact?Fargo%20(1996)"}}]}
```

それっぽいものが帰ってきたでしょうか。
（※戻ってきたjsonの形式がparseエラーになってしまっていましたが、そのまま表示しています）

## Reuters Newswire Recommendation

Reuters 21578 datasetを使った、類似文章のレコメンデーションです。

* [Recomendation Example \| Seldon Documentation](http://docs.seldon.io/content-recommendation-example.html)

データを読み込み、学習モデルを作成します。

```
$ cd /cygdrive/c/seldon-server/kubernetes/conf/examples/reuters

$ kubectl create -f import-data-job.json
job "reuters-import-data" created

(2～3分)

$ kubectl get jobs -l job-name=reuters-import-data
NAME                  DESIRED   SUCCESSFUL   AGE
reuters-import-data   1         1            2m

```

学習モデルをmicroserviceとして起動します。

```
$ start-microservice --type recommendation --client reuters -i reuters-example seldonio/reuters-example:2.0.7 rest 1.0
[Microservice(reuters-example,seldonio/reuters-example:2.0.7,rest,1.000000)]
Replicas is  1
kubectl apply -f c:/seldon-server/kubernetes/bin/../conf/microservices/microservice-reuters-example.json
deployment "reuters-example" configured
service "reuters-example" configured
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Writing data to file[/seldon-data/conf/zkroot/all_clients/reuters/alg_rectags/_data_]
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
updated zk node[/all_clients/reuters/alg_rectags]

$ kubectl get pods -l name=reuters-example
NAME                              READY     STATUS    RESTARTS   AGE
reuters-example-327623259-s9rgg   1/1       Running   0          3s
```

レコメンドを行います。

```
$ seldon-cli --quiet api --client-name reuters --endpoint  /js/recommendations --item 6020 --limit 3　| jq .
```

```json
{
  "size": 3,
  "requested": 3,
  "list": [
    {
      "id": "11348",
      "name": "",
      "type": 1,
      "first_action": 1498640773000,
      "last_action": 1498640773000,
      "popular": false,
      "demographics": [],
      "attributes": {},
      "attributesName": {
        "recommendationUuid": "2",
        "title": "GANDALF <GANDF> ACQUIRES STAKE IN DATA/VOICE",
        "body": "Gandalf Technologies Inc said it\nacquired a significant minority equity interest in privately\nheld Data/Voice Solutions Corp, of Newport Beach, Calif., for\nundisclosed terms.\n    Gandalf did not specify the size of the interest.\n    Data/Voice is a three-year-old designer and manufacturer of\na multiprocessor, multiuser MS-DOS computing system that\nGandalf plans to integrate w
ith its private automatic computer\nexchange information system, Gandalf said.\n Reuter\n\u0003"
      }
    },
    {
      "id": "7816",
      "name": "",
      "type": 1,
      "first_action": 1498640772000,
      "last_action": 1498640772000,
      "popular": false,
      "demographics": [],
      "attributes": {},
      "attributesName": {
        "recommendationUuid": "2",
        "title": "CELINA <CELNA> SHAREHOLDERS APPROVE SALE",
        "body": "Celina Financial Corp said\nshareholders at a special meeting approved a transaction in\nwhich the company transferred its interest in three insurance\ncompanies to a wholly owned subsidiary which then sold the\nthree companies to an affiliated subsidiary.\n    It said the company's interests in West Virginia Fire and\nCasualty Co, Congregation Insurance co and National Term Life\nI
nsurance Co had been transferred to First National Indemnity\nCo, which sold the three to Celina Mutual for cash, an office\nbuilding and related real estate.\n Reuter\n\u0003"
      }
    },
    {
      "id": "8571",
      "name": "",
      "type": 1,
      "first_action": 1498640772000,
      "last_action": 1498640772000,
      "popular": false,
      "demographics": [],
      "attributes": {},
      "attributesName": {
        "recommendationUuid": "2",
        "title": "AVALON <AVL> STAKE SOLD BY DELTEC",
        "body": "Avalon Corp said that <Deltec\nPanamerica SA> has arranged to sell its 23 pct stake in Avalon\nand that Deltec's three representatives on Avalon's board had\nresigned.\n    An Avalon spokeswoman declined to indentify the buyer of\nDeltec's stake or give terms of the sale.\n    In addition, Avalon said three other directors resigned. It\nsaid Benjamin W. Macdonald, a director of <TMO
C Resources Ltd>,\nthe principal holder of Avalon stock, and Hardwick Simmons, a\nvice chairman of Shearson Lehman Bros Inc, were then named to\nthe board.\n Reuter\n\u0003"
      }
    }
  ]
}
```

## Iris Classification

Irisデータセットを使ったクラス分類(Prediction)のサンプルです。

* [Prediction Example \| Seldon Documentation](http://docs.seldon.io/prediction-example.html)

実行の仕方に一貫性がありませんが、こちらのサンプルではデータ読み込みや学習部分の処理が別れておらず、`start-microservice`で実行する[Dockerfileの中でデータインポートや学習処理も含めてやっている](https://github.com/SeldonIO/seldon-server/blob/master/docker/examples/iris/xgboost/Dockerfile.in)ようです。

```
$ start-microservice --type prediction --client test -i iris-xgboost seldonio/iris_xgboost:2.1 rest 1.0
[Microservice(iris-xgboost,seldonio/iris_xgboost:2.1,rest,1.000000)]
Replicas is  1
kubectl apply -f c:/seldon-server/kubernetes/bin/../conf/microservices/microservice-iris-xgboost.json
deployment "iris-xgboost" created
service "iris-xgboost" created
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Writing data to file[/seldon-data/conf/zkroot/all_clients/test/predict_algs/_data_]
Added prediction algs for test
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
updated zk node[/all_clients/test/predict_algs]

(1～2分)

$ kubectl get pods -l name=iris-xgboost
NAME                            READY     STATUS    RESTARTS   AGE
iris-xgboost-1750015301-v3lxz   1/1       Running   0          1m
```

クラス分類を行います。

```
$ seldon-cli --quiet api --client-name test --endpoint /js/predict --json '{"data":{"f1":1,"f2":2.7,"f3":5.3,"f4":1.9}}' | jq .
```


```json
{
  "meta": {
    "puid": "2e43f2625611c7d3317acb33e5537a8fdfcf01dd",
    "modelName": "model_xgb",
    "variation": "iris-xgboost"
  },
  "predictions": [
    {
      "prediction": 0.00252304,
      "predictedClass": "Iris-setosa",
      "confidence": 0.00252304
    },
    {
      "prediction": 0.00350009,
      "predictedClass": "Iris-versicolor",
      "confidence": 0.00350009
    },
    {
      "prediction": 0.993977,
      "predictedClass": "Iris-virginica",
      "confidence": 0.993977
    }
  ],
  "custom": null
}

```

## TensorFlow Deep MNIST

最後に、[TensorFlowの手書き文字認識](https://www.tensorflow.org/get_started/mnist/pros)をSeldonで動かすデモをやってみましょう。

* [TensorFlow Deep MNIST Demo \| Seldon Documentation](http://docs.seldon.io/tensorflow-deep-mnist-example-docker.html)

```
$ cd /cygdrive/c/seldon-server/kubernetes/conf/examples/tensorflow_deep_mnist

$ kubectl create -f load-model-tensorflow-deep-mnist.json
job "load-model-tensorflow-deep-mnist" created

(2～3分)

$ kubectl get jobs | grep load-model
load-model-tensorflow-deep-mnist   1         1            3m
```

今回はトレーニング済の機械学習モデル(`load-model-tensorflow-deep-mnist.json`)を利用しましたが、TensorFlowを使って学習するところからやりたい場合は`train-tensorflow-deep-mnist.json`を指定してください。[^5]

[^5]: 私の環境では1時間くらいかかりました。

学習モデルが出来たらmicroservice(REST API)として起動します。

```
$ seldon-cli client --action setup --db-name ClientDB --client-name deep_mnist_client
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Trying to create the client
API DB has already been added to the MySQL DB 'ClientDB'
Adding JS consumer key for client 'deep_mnist_client' : '2M5B4Z6PRM032QDK0AGS'
Adding REST API key for client 'deep_mnist_client' : consumer_key='6WAKZ1UNIOOJ98F1W62B' consumer_secret='Y1Q12S9YLAW3PQ82A336'
Writing data to file[/seldon-data/conf/zkroot/all_clients/deep_mnist_client/_data_]
updated zk node[/all_clients/deep_mnist_client]
Adding grafana dashboard, response code 200

$ start-microservice --type prediction --client deep_mnist_client -p tensorflow-deep-mnist /seldon-data/seldon-models/tensorflow_deep_mnist/1/ rest 1.0
[Pipeline(tensorflow-deep-mnist,/seldon-data/seldon-models/tensorflow_deep_mnist/1/,rest,1.000000)]
Replicas is  1
kubectl apply -f c:/seldon-server/kubernetes/bin/../conf/microservices/microservice-tensorflow-deep-mnist.json
deployment "tensorflow-deep-mnist" created
service "tensorflow-deep-mnist" created
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
Writing data to file[/seldon-data/conf/zkroot/all_clients/deep_mnist_client/predict_algs/_data_]
Added prediction algs for deep_mnist_client
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
updated zk node[/all_clients/deep_mnist_client/predict_algs]
```

続いて、手書き文字の入力用GUIになるWebアプリを配備します。

* [Tensorflow Deep MNIST Webapp \| Seldon Documentation](http://docs.seldon.io/tensorflow-deep-mnist-webapp.html)

WebAppに設定するseldonのパラメータ(ip, key, secret)を取得。

```
$ seldon-cli keys  --client-name deep_mnist_client --scope all
connecting to zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181 [SUCCEEDED]
[{"scope": "all", "secret": "Y1Q12S9YLAW3PQ82A336", "client": "deep_mnist_client", "db": "ClientDB", "key": "6WAKZ1UNIOOJ98F1W62B"}]

$ kubectl get services seldon-server
NAME            CLUSTER-IP   EXTERNAL-IP   PORT(S)                       AGE
seldon-server   10.0.0.187   <nodes>       80:30015/TCP,5000:30017/TCP   1h
```

以下の`<seldon-server-ip>`, `<key>`, `<secret>`の値をそれぞれ埋めて実行してください。

```
kubectl run deep-mnist-webapp --image=seldonio/deep_mnist_webapp:1.2 --port=80 --command -- "/run_webapp.sh" "<seldon-server-ip>" "<key>" "<secret>"
```

実行例を示します。"ContainerCreating"から"Running"になるまで、私の環境では2～3分かかりました。

```
$ kubectl run deep-mnist-webapp --image=seldonio/deep_mnist_webapp:1.2 --port=80 --command -- "/run_webapp.sh" "10.0.0.187" "6WAKZ1UNIOOJ98F1W62B" "Y1Q12S9YLAW3PQ82A336"

(2～3分)

$ kubectl get pod | grep deep-mnist-webapp
deep-mnist-webapp-1912876608-l4wbf           1/1       Running   0          2m

$ kubectl expose deployment/deep-mnist-webapp --type="LoadBalancer"
service "deep-mnist-webapp" exposed
```

あとは以下のコマンドでデプロイしたWebアプリのURLを取得し、ブラウザで開いてみてください。

```
c:\> minikube service deep-mnist-webapp --url
http://192.168.11.209:32244
```

アプリケーションが表示され、こんな感じで動作確認ができればOKです。

![aaaaa.gif](https://qiita-image-store.s3.amazonaws.com/0/50184/e9ff26c4-7251-ebff-7118-c502301e8c88.gif)

## Grafana

さいごに、Grafanaによる統計情報ダッシュボードを見てみましょう。
[kubernetes/conf/MAKEFILE](https://github.com/SeldonIO/seldonio.github.com/blob/master/install.md#grafana-and-spark-ui-passwords) の内容を変更していなければ、`admin/admin`でログインできます。

```
c:\> minikube service monitoring-grafana --url
http://192.168.11.201:30002
```

![0000.png](https://qiita-image-store.s3.amazonaws.com/0/50184/02bf15b4-0803-7a27-d040-158accafb4b0.png)

あとは「Dashboards」から情報を表示したいサンプルを選んでください。　

* ml100k (Content Recommendation API)

![11111.png](https://qiita-image-store.s3.amazonaws.com/0/50184/aca3f6d7-6341-6c93-050f-bc5abaa33c70.png)

* deep_mnist_client (Prediction API)

![2222.png](https://qiita-image-store.s3.amazonaws.com/0/50184/042e8a68-b133-e62b-3306-d5f263c4a904.png)

とくにPrediction APIが表示されるのは画面の下の方のため、スクロールしないとグラフが表示されないのでご注意ください。


# Tips

## ※Windows + Cygwin環境で動作確認を行う場合※

### Windows + Hyper-VでのMinikube環境構築

[別記事](http://qiita.com/terukizm/items/bf8e4744fcb8ba494fc8)にまとめました。同様の環境で試してみたい方はご参照いただければ幸いです。

### minikubeコマンドはコマンドプロンプトから叩く

Cygwinのターミナルからだと`Error starting host: Error creating host: Error executing step: Creating VM.: exit status 1.` となって失敗します。minikubeコマンドはコマンドプロンプト(かPowerShell)を管理者モードで起動し、そこから叩くようにしてください。

なお、kubectlコマンドの方はターミナル経由(及びシェル経由)でも問題なく動きました。

### 起動スクリプトの編集

Seldonのスクリプトはbash(seldon-up, seldon-cli)、およびpython(start-microservice)で書かれているため、実行するにはターミナル環境が必要になります。

Cygwinを使う場合、kubectlが/cygdirve/等のパスを解釈できないので、Seldonの起動スクリプト中でディレクトリパスをベタに上書きすることで対処しました。(例: `/cygdrive/c/seldon-server` → `c:/seldon-server`)

他のターミナル環境でも同様の対処が必要かは不明ですが、うまく動かない場合は参考にしてみてください。

```diff

$ git diff seldon-up
diff --git a/kubernetes/bin/seldon-up b/kubernetes/bin/seldon-up
index 7b68c3c..b7267e2 100755
--- a/kubernetes/bin/seldon-up
+++ b/kubernetes/bin/seldon-up
@@ -4,6 +4,7 @@ set -o nounset
 set -o errexit

 STARTUP_DIR="$( cd "$( dirname "$0" )" && pwd )"
+STARTUP_DIR="c:\\seldon-server\\kubernetes\\bin\\"

 SELDON_HOME=${STARTUP_DIR}/../..
 SELDON_WITH_SPARK=${SELDON_WITH_SPARK:-true}
```

```diff

$ git diff start-microservice
diff --git a/kubernetes/bin/start-microservice b/kubernetes/bin/start-microservice
index ea1006a..776fe27 100755
--- a/kubernetes/bin/start-microservice
+++ b/kubernetes/bin/start-microservice
@@ -140,6 +140,7 @@ class MicroserviceRunner(object):

     def __init__(self,replicas=1):
         self.script_folder = os.path.dirname(os.path.realpath(__file__))
+        self.script_folder = 'c:/seldon-server/kubernetes/bin'
         self.replicas = replicas
         print "Replicas is ",self.replicas
```

### minikube start時に以下のようなエラーが出る

何回かやっていたら出なくなったり、出た状態でも特に問題なく動いたりしました…
再現性も微妙でよくわかりません…

```
c:\> minikube start --vm-driver="hyperv" --memory=14000 --disk-size=40g
Starting local Kubernetes v1.6.4 cluster...
Starting VM...
Downloading Minikube ISO
 90.95 MB / 90.95 MB [==============================================] 100.00% 0s
E0628 17:06:57.323398    4632 start.go:127] Error starting host: Error creating host: Error executing step: Provisioning VM.
: ssh command error:
command : sudo hostname minikube && echo "minikube" | sudo tee /etc/hostname
err     : exit status 255
output  : .

 Retrying.
E0628 17:06:57.327396    4632 start.go:133] Error starting host:  Error creating host: Error executing step: Provisioning VM.
: ssh command error:
command : sudo hostname minikube && echo "minikube" | sudo tee /etc/hostname
err     : exit status 255
output  :
================================================================================
An error has occurred. Would you like to opt in to sending anonymized crash
information to minikube to help prevent future errors?
To opt out of these messages, run the command:
        minikube config set WantReportErrorPrompt false
================================================================================
```

## 処理が終わらない、試行錯誤してたらうまく動かなくなった

* `kubectl get all`
* `kubectl get events`
* `kubectl describe nodes` 

などのコマンドで、エラーが発生していないか確認してください。

エラーがあったら`seldon-down && seldon-up`でまるっと再起動させればうまく行くようになる場合もあります。

ただ、VMごと一旦全部消してやり直すのが一番確実かもしれません…

```
c:\> minikube stop
c:\> minikube delete
```

さらにminikubeの設定ファイル、キャッシュファイルも消しておくと安心です。

```
$ rm -rf ~/.minikube
```


## job完了の監視

やり方は色々あると思いますが、watchコマンドが不要なパターンを例示します。ご参考までに。
DESIREDとSUCCESSFULの値が同じになったら止まるようにするとか、色々やってみてください。

```
$ yes 'kubectl get jobs | grep ml100k; sleep 5' | sh
```

## Seldonの仕組みを理解する

提供されているサンプルはいくつかありますが、[TensorFlow Deep MNIST Demo](http://docs.seldon.io/tensorflow-deep-mnist-example-docker.html)が一番わかりやすいと感じました。こちらをざっくり動かしてみた上で、

* [Kuberetesで投げるジョブ](https://github.com/SeldonIO/seldon-server/tree/master/kubernetes/conf/examples/tensorflow_deep_mnist)
  * [Dockerfile](https://github.com/SeldonIO/seldon-server/tree/master/docker/examples/tensorflow_deep_mnist)
* [学習ロジック](https://github.com/SeldonIO/seldon-server/blob/master/docker/examples/tensorflow_deep_mnist/create_pipeline.py)
  * [公式の解説](http://docs.seldon.io/tensorflow-deep-mnist-example.html)

という順番で見ていくと雰囲気がつかみやすいのかな、と思いました。ご参考までに。


# 最後に

Seldonは荒削りな印象がありますが、全体を通してみると意外とシンプルで、とっつきやすいプロダクトだと感じました。私は最初Kubernetesが初体験でかなり戸惑ったのですが、Kubernetesの仕組みや考え方を理解できてからは、すんなり入っていくことができました。

「実際に運用レベルで使うことができるのか？」「自作の機械学習モデルを手間なく組み込めるのか？」「Seldonが規定するデータI/Fに合わせるのに困難は伴わないか？」「データの再学習はどうするのか？（MySQLに永続化されている？）」などなど気になる点も色々ありますが、その辺も調査しつつ、今後も触っていきたいなあと思います。

何より名前が素敵なので[^6]、ロマンを感じた方は、是非とも触ってみてください！

以上です。


[^6]: [動画](https://www.youtube.com/watch?v=G1dM12tCilE)によるとプロダクトの発音は、日本語表記の「セルダン」に近いようです。名前の由来は公式には発表されていないのですが、アイザック・アシモフの小説「ファウンデーション」に出てくる天才数学者[「ハリ・セルダン」](https://www.sideranch.co.jp/gingateikoku/img/minichara/seru_img01.png)から取られているのでしょうか…？
---