Alibaba Cloud コンテナサービスでTensorFlowとRAPIDSを使用し画像検索を行う方法

この記事では、NVIDIAが発表したGPUアクセラレーションライブラリ**「RAPIDS」**を使って、**Alibaba Cloud Container Service**上で画像検索の高速化を行う方法を紹介します。

著 Biran

#はじめに
アルゴリズム、データ、演算処理能力は、AIの3大要素です。演算処理能力がなければ、AIは実現できません。NVIDIA GPUの演算処理能力は、AIモデルのトレーニングを加速するための最良の選択となりますが、高価です。Alibaba Cloud Container Service、Elastic Container Instance（ECI）、Arenaで構成されるソリューションは、NVIDIA GPUのコンピューティングパワーを活用するための、簡単で効果的、かつコスト効率の高い方法を提供します。

ディープラーニングは、NVIDIA GPUに言及する際に誰もが最初に思い浮かべるものです。従来の機械学習やデータ分析の手法では、GPUを使うことはほとんどありません。実はNVIDIAには、データサイエンスや機械学習のためにNVIDIAが立ち上げたGPUアクセラレーションライブラリであるRAPIDSという優れたプロジェクトがあります。RAPIDSの詳細については、[RAPIDSの公式サイト](https://rapids.ai/?spm=a2c65.11461447.0.0.84a060d2dnhAMD)をご覧ください。このプロジェクトは、従来のアルゴリズムにGPUによる高速化をもたらすことを目的としており、Pandasやscikit-learnと同様の操作性やユーザーエクスペリエンスを提供します。RAPIDSには、Pandasに相当するcuDF、scikit-learnに相当するcuML、グラフデータを処理するcuGraphの3つのモジュールがあります。その親和性の高さから、RAPIDSと深層学習フレームワークを組み合わせ、cuDFを使ってGPUによるデータ処理を高速化し、TensorFlowやPyTorchといった深層学習モデルフレームワークを使ってタスクを実行することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/ef7e6b8e-0e71-e785-bd1f-6ea2ab0e0c2e.png)

この記事では、TensorFlowとRAPIDSを使ってAlibaba Cloud Container Service上で画像による検索を行い、ECIを使ってGPUリソースを申請する方法を紹介します。GPUリソースは数秒で準備され、不要になったら解放されます。つまり、事前にGPUインスタンスを用意する必要はありません。また、Kubernetesのインフラを扱う必要もありません。Arenaコマンドを実行して、GPUを含むRAPIDS環境を構築・実行し、GPUインフラを管理します。

#手順
###ステップ1：クラスターの準備
はじめに、マネージドKubernetesクラスターを準備します。マネージドKubernetesクラスターは、Alibaba Cloudのリソース上で稼働するノードを管理し、Alibaba CloudがO&M（運用・保守）コストを負担します。一方、このクラスターでは、仮想Kubeletノードを作成します。

すでにコンテナサービス用のKubernetesクラスターが作成されている場合は、マネージドKubernetesクラスターを選択します。

システムコンポーネントコンテナを実行する必要があるため、ノードには少なくとも1つのワーカーノードが含まれている必要があります。

1) 仮想ノードのインストールの詳細については、「Virtual Nodes Documentation」を参照してください。

2) virtual-kubelet-autoscalerを設定します。クラスタ内のGPUリソースが不足している場合は、virtual-kubelet-autoscalerを使用して、GPUを使用しているECIコンテナグループを削除します。

###ステップ2：Arenaを実行してRAPIDSサービスを作成する
1) 以下のコマンドを使用してArenaをインストールします。

```
$ wget http://kubeflow.oss-cn-beijing.aliyuncs.com/arena-installer-0.3.0-b556a36-linux-amd64.tar.gz
$ tar -xvf arena*.tar.gz
$ cd arena-installer
$ ./install.sh
```

2) 次に、Arenaコマンドを実行してクラスタのGPUリソースを確認します。次のスニペットが示すように、このユーザーのクラスターには1つのリアルノードがあり、GPUリソースは含まれていません。また、物理的には存在しないため課金されない仮想ノードが存在します。このノードは，無制限の GPU リソースを提供しており，スケーリングも可能です．


```
$ arena top node
arena top node
NAME                       IPADDRESS      ROLE    STATUS  GPU(Total)  GPU(Allocated)
cn-shanghai.192.168.1.248  192.168.1.248  <none>  ready   0           0
virtual-kubelet            172.20.2.18    agent   ready   1000        0
-----------------------------------------------------------------------------------------
Allocated/Total GPUs In Cluster:
0/1000 (0%)
```

3) RAPIDS タスクを投入する前に、必要な準備を行い、作成プロセスの高速化とアクセス操作の簡略化を図ります。

3.1 LoadBalancer にアクセスする方法を設定します。なお、この方法は簡略化のためにのみ使用しています。本番環境への外部 IP アドレスからのアクセスを禁止することを推奨します。

```
$ find /charts/ -name "*.yaml" | xargs sed -i "s/NodePort/LoadBalancer/g"
```

3.2 次に、以下の手順で起動速度を高速化します。

3.2.1 GPUコンテナイメージは通常、非常に大きなサイズです。例えば，今回の実験で使用したRAPIDSコンテナ・イメージは14.7GBの容量があります。一般的に、起動時間は約10分です。しかし，イメージキャッシング機能を使えば，この時間を20秒に短縮することができます。

```
docker images | grep rapids
registry.cn-shanghai.aliyuncs.com/tensorflow-samples/rapids-samples                0.8.2-cuda10.0-runtime-ubuntu16.04   4597a0334d41        12 days ago         14.7GB
```

3.2.2 サーバーレスKubernetesでは、ImageCache CRDを作成するだけで、画像キャッシング機能を直接利用できます。

```
$ cat > imagecache.yaml << EOF
apiVersion: eci.alibabacloud.com/v1
kind: ImageCache
metadata:
  name: imagecache-rapids
spec:
  images:
  - registry.cn-shanghai.aliyuncs.com/tensorflow-samples/rapids-samples:0.8.2-cuda10.0-runtime-ubuntu16.04
  imageCacheSize:
   50
EOF

$ kubectl create -f imagecache.yaml
```

3.2.3 送信後、しばらく待ってみましょう。ImageCache の状態を確認します。CACHEID は、以前のタスクをサブミットする際に指定された snapshot-id を使用している可能性があります。

```
$ kubectl get imagecache
NAME                AGE    CACHEID                    PHASE   PROGRESS
imagecache-rapids   3d9h   imc-uf6dxdji7txxxxx        Ready   100%
```

4) RAPIDSの開発環境を以下のように提出します。

```
$ arena serve custom \
     --name=rapids \
     --selector=type=virtual-kubelet \
     --toleration=all \
     --annotation=k8s.aliyun.com/eci-image-snapshot-id=imc-uf6dxdji7txxxxx \
     --annotation=k8s.aliyun.com/eci-instance-type=ecs.gn5i-c8g1.2xlarge \
     --gpus=1 \
     -e=PASSWORD=mypassw0rd \
     --restful-port=80 \
     --image=registry.cn-shanghai.aliyuncs.com/tensorflow-samples/rapids-samples:0.8.2-cuda10.0-runtime-ubuntu16.04
configmap/rapids-201912011815-custom-serving created
configmap/rapids-201912011815-custom-serving labeled
service/rapids-201912011815 created
deployment.extensions/rapids-201912011815-custom-serving created
```

前述のコードスニペットで使用されているコマンドを簡単に見てみましょう。

- `--selector=type=virtual-kubelet`: [仮想ノード](https://www.alibabacloud.com/help/doc-detail/119207.htm?spm=a2c65.11461447.0.0.84a060d2dnhAMD)を使用してポッドを起動することを示します。
- `--annotation=k8s.aliyun.com/eci-instance-type=ecs.gn5i-c8g1.2xlarge`: ECI コンテナグループのタイプを指定します。
- `ecs.gn5i-c8g1.2xlarge`：ECIコンテナグループのタイプを指定します。Alibaba CloudのP4モデルを指します。詳細な仕様については、関連[ドキュメント](https://www.alibabacloud.com/help/doc-detail/108496.htm?spm=a2c65.11461447.0.0.84a060d2dnhAMD)を参照してください。
- `--annotation=k8s.aliyun.com/eci-image-snapshot-id=imc-uf6dxdji7txxxxx`: 手順 3.2.3 の CACHEID を指定します。
- `-e=PASSWORD=mypassw0rd: E=PASSWORD=mypassw0rd`：環境変数PASSWORDを設定してRAPIDSノートブックにアクセスすることを示します。
- `--e=PASSWORD=mypassw0rd`：PASSWORD環境変数を設定することで、RAPIDSノートにアクセスすることを示します。

5) ENDPOINT_ADDRESSとPORTSの組み合わせであるアクセスアドレスを表示します。この例では、`106.15.173.2:80`です。同時に、このタスクが32秒でRunning状態に切り替わることも確認してください。

```
$ arena serve list
NAME    TYPE    VERSION       DESIRED  AVAILABLE  ENDPOINT_ADDRESS  PORTS
rapids  CUSTOM  201911181827  1        1          105.13.58.3      restful:80

$ arena serve get rapids
 arena serve get rapids
NAME:             rapids
NAMESPACE:        default
VERSION:          201912011815
DESIRED:          1
AVAILABLE:        1
SERVING TYPE:     CUSTOM
ENDPOINT ADDRESS: 106.15.173.2
ENDPOINT PORTS:   restful:80
AGE:              32s

INSTANCE                                           STATUS   AGE  READY  RESTARTS  NODE
rapids-201912011815-custom-serving-6b54d5cd-swcwz  Running  32s  1/1    0         N/A
```

6) クラスタのGPU使用率を再度確認します。GPUリソースがすでに使用されていることに注意してください。

```
$ arena top node
NAME                       IPADDRESS      ROLE    STATUS  GPU(Total)  GPU(Allocated)
cn-shanghai.192.168.1.248  192.168.1.248  <none>  ready   0           0
virtual-kubelet            172.20.2.20    agent   ready   1000        1
-----------------------------------------------------------------------------------------
Allocated/Total GPUs In Cluster:
1/1000 (0%)
```

7) このGPUを使用しているポッドを問い合わせるには、元のコマンドに「-d」を追加して、特定のポッド名を表示します。

```
$ arena top node -d


NAME:       cn-shanghai.192.168.1.248
IPADDRESS:  192.168.1.248
ROLE:       <none>

Total GPUs In Node cn-shanghai.192.168.1.248:      0
Allocated GPUs In Node cn-shanghai.192.168.1.248:  0 (0%)
-----------------------------------------------------------------------------------------

NAME:       virtual-kubelet
IPADDRESS:  172.20.2.20
ROLE:       agent

NAMESPACE  NAME                                                GPU REQUESTS
default    rapids-201912011815-custom-serving-6b54d5cd-swcwz  1

Total GPUs In Node virtual-kubelet:      1000
Allocated GPUs In Node virtual-kubelet:  1 (0%)
-----------------------------------------------------------------------------------------


Allocated/Total GPUs In Cluster:  1/1000 (0%)
```

8) 手順4のアクセスアドレスとポートを使って、ローカルブラウザでアドレスにアクセスします。`http://{ENDPOINT ADDRESS}:{ENDPOINT PORT}`と入力します。この例では、アドレスは `http://105.13.58.3:80` です。

注：Chromeブラウザの使用をお勧めします。

9) 前述のコマンドで設定したログオンパスワードを入力して、「Log in」ボタンをクリックします。この例では、パスワードは`mypassw0rd`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/e10c244e-bdac-d8d1-155e-e2dceb688d85.png)



###ステップ3： 画像検索デモの実行
1) デモが置かれているcumlディレクトリに移動します。
2) `cuml_knn.ipynb`ファイルをダブルクリックします。
3) スタートアイコンをクリックします。

注：1回クリックすると1つのセルが実行されます。デモが完全に実行されるまでクリックしてください。より詳しい説明は、次の「デモの実行プロセス」を参照してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/cbe76c24-acce-7986-8add-8c20eb04285c.png)


**デモ実行プロセス**
画像検索のデモは、以下の3つのステップで構成されています。デモ結果では、GPUアクセラレーションによるRAPIDS cuml KNNと、CPUのみを使用するscikit-learn KNNの性能の違いを示しています。

1) データセットの処理

1.1) データセットのダウンロードと圧縮解除

本デモでは、[STL-10](https://cs.stanford.edu/~acoates/stl10/?spm=a2c65.11461447.0.0.84a060d2dnhAMD)データセットに、96×96×3のサイズで10万枚のラベルのない画像を入れています。画像の特徴を抽出するために他のデータセットを使用しますが、これらのデータセットは同じサイズの画像を使用するようにしてください。

`download_and_extract(data_dir)`メソッドを使って、STL-10データセットをダウンロードし、解凍します。RAPIDSのイメージでは、データセットは `./data directory`にダウンロードされます。データセットの解凍には`download_and_extract()`メソッドを使用します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/7b8587c1-a3d5-9c32-b517-19bcde0cc9d7.png)

1.2) 画像を読み取る

データセットから解凍されたデータはバイナリです。`read_all_images(path_to_data)`メソッドを使ってデータを読み込み、NHWCフォーマット（バッチ、高さ、幅、チャンネル）に変換します。このフォーマットでは、Tensorlowが画像の特徴を抽出することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/ad3d1b51-4047-2e6a-a2d4-ea119f010235.png)


1.3) 画像を表示する

`show_image(image)`メソッドを使って、データセットの中からランダムな画像を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/a85ee55c-f717-58fb-348c-1b5f252e633d.png)


1.4) データセットを分割する

データセットを9:1の割合で2つの部分に分割します。1つは画像インデックスライブラリを作成するため、もう1つは画像を検索するために使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/ee6b1084-439c-bf20-a379-14f269011488.png)


2）画像の特徴を抽出する

TensorFlowとKerasを使って、画像の特徴を抽出します。ImageNetデータセットに基づいて事前に学習されたモデルResNet50(notop)を使用します。

2.1）TensorFlowのパラメータを設定する

デフォルトでは、TensorFlowはすべてのGPUメモリ容量を使用します。cuML用に一部のGPUメモリ容量を確保しておきます。以下の方法でGPUメモリのパラメータを設定します。

- 方法1：運用要件に応じてメモリを割り当てます。

```
config.gpu_options.allow_growth = True
```

- 方法2：比率を設定してTensorFlowが使用するメモリの量を決めます。
このデモでは方法2を使用し、比率を0.3に設定しています。これは、TensorFlowがGPUメモリの30%を使用する可能性があることを示しています。必要に応じて比率を調整してください。

```
config.gpu_options.per_process_gpu_memory_fraction = 0.3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/b2592fa9-b676-a292-71b4-78cb895e6d68.png)



2.2 事前学習済みのモデルResNet50(notop)をダウンロードします。TensorFlowをパブリックネットワークに接続し、モデルをダウンロードします。モデルのサイズは約91MBです。モデルは、` /root/.keras/models/ directory`にダウンロードされます。

|パラメータ|	説明|
|:-----------|:-------------------:|
|weights	| 有効値:　- weightをランダムな値に初期化します。　　　　　　　　　　　　　　　　　　　　　　　- imagenet：ImageNetで事前学習されたモデルのweightに初期値を設定します。このデモでは、このパラメーターはimagenetに設定されています。|
|include_top|有効値:- True：ResNet50のネットワーク構造全体を含む、最後の完全連結層です。- False: このデモでは、ニューラルネットワークモデルResNet50を使って、画像を分類するのではなく、画像の特徴を抽出しています。そのため、このパラメータを False に設定します。|
|input_shape|	このオプション・パラメータは、画像の入力形状を指定します。include_topパラメータがFalseに設定されている場合にのみ有効です。画像の幅と高さは32ミリ以上でなければなりません。このパラメータを（96, 96, 3）に設定します。
|pooling|	include_topパラメータがFalseに設定されている場合は、プーリングレイヤーモデルを設定する必要があります。有効値:- None: 4次元テンソルを出力します。 - avg: 平均的なプーリングを示し、2次元テンソルを出力します。max：プーリングの最大値を示し、2Dテンソルを出力します。本デモではmaxに設定されています。|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/6412308d-b117-eaf0-62ca-492ad88ea5c1.png)




それでは、`model.summary()`メソッドを実行して、モデルのネットワーク構造を確認してみましょう。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/93d0d314-50e7-a07b-8e6e-ac26608f1026.png)


2.3 画像特徴の抽出

分割したデータセットで`model.predict()`メソッドを呼び出し、画像の特徴を抽出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/8c5a50a5-dd7f-855e-56ce-af8506a50c47.png)




3) 類似画像の検索

3.1 cuML KNNを使って類似画像を検索する。

Kを3に設定すると`（k=3）`、最も似ている3枚の画像を検索します。Kの値は必要に応じて調整してください。指標の作成には`knn_cuml.fit()`メソッドを使用します。`knn_cuml.kneighbors()`メソッドを使って近傍探索を行います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/63ecc9f2-92c6-ba96-deef-cf6d611cc836.png)


KNNがベクターを検索するのにかかる時間は791ミリ秒です。

scikit-learnのKNNを使って、類似画像を検索します。Kを3に設定し`（n_neighbors=3）`、`n_jobs=-1`を指定することで、すべてのCPUを使って最近傍の画像を検索します。

注： `ecs.gn5i-c8g1.2xlarge`モデルは8つのvCPUで構成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/9ac164c1-167d-e642-21ac-f84e5c6bcbfc.png)


KNN がベクターを検索するのにかかる時間は 7 分 34 秒です。

cuML KNNとscikit-learn KNNの検索結果を比較してみましょう。cuML KNN と scikit-learn KNN のベクトル検索速度を比較します。GPUアクセラレーションを用いたcuML KNNは791ミリ秒しかかかりませんが、CPUを用いたscikit-learn KNNは7分34秒かかります。cuML KNNの検索速度は、scikit-learn KNNの600倍近くになります。

cuML KNNとscikit-learn KNNの検索結果が同じであるかどうかを確認します。以下の出力配列を比較してみてください。

- Distance:K個の最小距離値。Kは距離値の数を表します。このデモでは、1万枚の画像を検索しています。Kの値は3なので、distance.shape=(10000,3)となります。
- Indices: 対応する画像のインデックス。indices.shape=(10000, 3)。


このデモで使用したデータセットには，同一の画像が含まれています．同一の画像であっても，インデックスが異なる場合があります．そのため，結果を比較する際には，インデックスではなく，distanceを用いる必要があります。計算上の偏差が存在する可能性があります。10,000枚の画像に対する3つの最小ディスタンス値の偏差が、どちらの方法でも1より小さければ、結果は同一であると考えられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/80a8c083-1c57-21a7-3c74-4a2d640d3bc0.png)


**画像検索結果**
このデモでは、10,000枚の画像の中からランダムに5枚の画像を選び、その画像を使って類似した画像を検索しています。検索結果は5行4列で表示されます。

1列目には、元の5枚の画像が表示されています。2列目、3列目、4列目には類似画像が表示されます。2列目から4列目にかけて、元画像と類似画像の類似度が下がっていきます。各類似画像のタイトルは，計算された距離である．値が大きいほど、類似性が低いことを示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/961cfd79-2973-74df-412a-5e672127df1d.png)


###ステップ4: クリーンアップ
以下のコマンドを実行して、クリーンアップを行います。

```
$ arena serve delete rapids
service "rapids-201912011815" deleted
deployment.extensions "rapids-201912011815-custom-serving" deleted
configmap "rapids-201912011815-custom-serving" deleted
INFO[0000] The Serving job rapids with version 201912011815 has been deleted successfully
```

#概要
この記事では、[Arena](https://github.com/kubeflow/arena?spm=a2c65.11461447.0.0.84a060d2dnhAMD)とAlibaba Cloud [Serverless Kubernetes](https://www.alibabacloud.com/help/doc-detail/86366.htm?spm=a2c65.11461447.0.0.84a060d2dnhAMD)を利用して、RAPIDSアクセラレーションデータサイエンスソリューションを低コストで高速かつシンプルに利用する方法を体験できます。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/experience-rapids-data-science-acceleration-in-alibaba-cloud-container-service_595841)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*