PyTorchのDL ModelをWatson MLで動かす

# はじめに
以下の3つの記事の応用問題として「**Google Colabで学習したPyTorchのモデルをWatson ML上で動かす(予測する)**」ということを試したところ、比較的簡単にできたので、その手順メモを残します。

1. [Watson Studioでscikit-learn機械学習モデルをWebサービス化する](https://qiita.com/makaishi2/items/38371d272d752b6e7647)
2. [PyTorch 学習メモ (Karasと同じモデルを作ってみた)](https://qiita.com/makaishi2/items/3676d216fe9b34b63430)
3. [(手順メモ) Watson Studio Jupyterでデータアセットに登録したデータをローカルにコピーする](https://qiita.com/makaishi2/items/e383f4c114fc0fd93bd8)

# PyTorchのモデルをWatson MLで動かす場合の前提
PyTorchのモデルをWatson MLで動かす場合は、モデルをONNX形式でexportし、更にgzipで圧縮をかけます。
今回は、このgzファイルをいったんローカルPCにダウンロードし、更にWatson Studioのデータアセットとして登録する方法をとりました。
具体的な手順としては、2. [PyTorch 学習メモ (Karasと同じモデルを作ってみた)](https://qiita.com/makaishi2/items/3676d216fe9b34b63430)
のNotebookをGoogle Colab上でそのまま動かし、更にその下に下記のセルを追加して、実行しました。

```py3
# ダミーデータの作成
dummy_input = torch.randn((1, 3, 32, 32)).to(device)

# onyx形式でexport
# keep_initializers_as_inputsのオプションが重要でこれがないとエラーになる
torch.onnx.export(net, dummy_input, "cifar10-pytorch-sample.onnx", 
                  keep_initializers_as_inputs=True, verbose=True)

# gz形式に圧縮
!tar czvf cifar10-pytorch-sample.gz cifar10-pytorch-sample.onnx

# 結果をPCにダウンロード
fn = 'cifar10-pytorch-sample.gz'
from google.colab import files
files.download(fn) 
```

ポイントとして、``torch.onnx.export``関数呼び出しのときに、``keep_initializers_as_inputs``オプションを付ける必要がありました。この指定をしないと、モデル呼び出し時にランタイムエラーになってしまいます。

# Watson Studio側用Notebook
ここから先の作業はWatson Studio側になります。
デプロイ作業と、予測作業の両方を、以下のNotebookを使って行いました。

https://github.com/makaishi2/sample-data/blob/master/notebooks/pytorch%20deploy%20sample.ipynb

URL指定で新規Watson Studio Notebookを作る場合は、下記リンクをコピペして下さい。

``https://raw.githubusercontent.com/makaishi2/sample-data/master/notebooks/pytorch%20deploy%20sample.ipynb
``
読み込んだ直後の状態は下記のようになります。

![スクリーンショット 2021-01-12 15.56.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/a144132b-996f-0e18-7960-a3cf89354334.png)


# Notbeookを動かす前の準備
以下のことが必要になります。

1. Watson Studioのインスタンス作成
2. Watson Studioのプロジェクト作成
3. アクセストークンが作成済み
4. デプロイメントスペースが作成済み
5. PCにダウンロードしたファイル ``cifar10-pytorch-sample.gz``をデータアセットとしてアップロード済み

それぞれの手順については、以下の記事を参考にして下さい。

1.と2.: [無料でなんでも試せる! Watson Studioセットアップガイド](https://qiita.com/makaishi2/items/b666f847a98c2e42dfbb)
3. : [(手順メモ) Watson Studio Jupyterでデータアセットに登録したデータをローカルにコピーする](https://qiita.com/makaishi2/items/e383f4c114fc0fd93bd8)
4. : [デプロイメントスペースの作成](https://qiita.com/makaishi2/items/e5ad4d068bd364fdc056#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%83%A1%E3%83%B3%E3%83%88%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90)

5に関しては 「**プロジェクトに追加**」->「**データ**」から画面の指示に従い、ファイルをdrag and dropすればいいです。

# Notebook解説
あとは、ほとんど上で紹介した3つの記事の復習です。概要のみの説明にとどめ、PyTorch固有の話だけ、詳しめに解説します。

## 一番上のセル
下記の一番上のセルは、ツールが自動生成したものです。データアセットをローカルにコピーするときに必要になります。
詳しい手順は、[(手順メモ) Watson Studio Jupyterでデータアセットに登録したデータをローカルにコピーする](https://qiita.com/makaishi2/items/e383f4c114fc0fd93bd8) を参照して下さい。

```
# @hidden_cell
# The project token is an authorization token that is used to access project resources like data sources, connections, and used by platform APIs.
from project_lib import Project
project = Project(project_id='xxxx', project_access_token='xxxx')
pc = project.project_context
```

## ローカルのモデルファイルのコピー
以下のコードがデータアセットにあるモデルファイルをローカルにコピーするものです。
解説は、[(手順メモ) Watson Studio Jupyterでデータアセットに登録したデータをローカルにコピーする](https://qiita.com/makaishi2/items/e383f4c114fc0fd93bd8)
にありますので、こちらを参照して下さい。

```py3
# project-libを使ってデータアセットのファイルをローカルにコピーする

fn = 'cifar10-pytorch-sample.gz'
infile = project.get_file(fn)
with open(fn, 'wb') as local_file:
    local_file.write(infile.read())
```

## 1. モデルの保存
この手順は、上で紹介した記事[Watson Studioでscikit-learn機械学習モデルをWebサービス化する](https://qiita.com/makaishi2/items/38371d272d752b6e7647)とほとんど同じ手順です。

差分として考慮が必要な箇所として「**1.3 Software Specification ID の取得**」がありますが、結論として上の記事と同じ``"default_py3.7"``を選べばよかったです。

「**1.4 モデルの保存**」のコードは次のようになります。(見やすいようにnotebookから一部修正しています)

```py3
model_path = 'cifar10-pytorch-sample.gz'

metadata = {
            client.repository.ModelMetaNames.NAME: 'External pytorch model',
            client.repository.ModelMetaNames.TYPE: 'pytorch-onnx_1.3',
            client.repository.ModelMetaNames.SOFTWARE_SPEC_UID: sofware_spec_uid
}

published_model = client.repository.store_model(
    model=model_path,
    meta_props=metadata)
```

scikit-learnの時は、store_model関数の引数として、学習済みモデルのインスタンスそのものを引数にした箇所が、今回は、外部ファイル名(実体はONNX形式のモデルを圧縮したもの)が引数になっています。

正常にモデルの登録ができると、下記のようなコマンドに対して、モデルが表示されます。

```py3
models_details = client.repository.list_models()
```


```
------------------------------------  -------------------------  ------------------------  -----------------
ID                                    NAME                       CREATED                   TYPE
f25e3664-b2d3-476b-94ad-0f17868f82b9  External pytorch model     2021-01-12T06:08:18.002Z  pytorch-onnx_1.3
dfc072c0-3dad-42b0-9951-4827271fd868  Scikit IRIS random forest  2021-01-11T06:32:59.002Z  scikit-learn_0.23
------------------------------------  -------------------------  ------------------------  -----------------
```

## 2. モデルのデプロイ
モデルのデプロイもscikit-learnの時とまったく同じです。解説は省略し、コードと実行結果のみ記載します。


#### コード

```py3
metadata = {
    client.deployments.ConfigurationMetaNames.NAME: "Deployment of external pytorch model",
    client.deployments.ConfigurationMetaNames.ONLINE: {}
}

created_deployment = client.deployments.create(published_model_uid, meta_props=metadata)
```

#### 実行結果

```
#######################################################################################

Synchronous deployment creation for uid: 'f25e3664-b2d3-476b-94ad-0f17868f82b9' started

#######################################################################################


initializing
ready


------------------------------------------------------------------------------------------------
Successfully finished deployment creation, deployment_uid='d2748d63-7b21-49a1-8533-956b421498ca'
------------------------------------------------------------------------------------------------
```

#### コード

```py3
# デプロイメントの一覧表示

client.deployments.list()
```

#### 実行結果

```
-----------------------------------  ------------------------------------  -----  ------------------------
GUID                                  NAME                                  STATE  CREATED
d2748d63-7b21-49a1-8533-956b421498ca  Deployment of external pytorch model  ready  2021-01-12T06:08:41.907Z
864aec72-15c1-4008-b96a-6ce1c1776af4  Scikit-Learn Iris Model online        ready  2021-01-11T06:46:10.762Z
------------------------------------  ------------------------------------  -----  ------------------------
```

次のコードは、``deployment_uid``変数を設定するためのものです。この変数は予測時に必要です。
今は、デプロイをした直後なので、``created_deployment``変数から取得可能ですが、予測のみを行うコードの場合、この変数が使えません。その場合、上の一覧表示の結果をコピーして、手動で設定することになります。

#### コード

```py3
# depolyment_uid 取得

deployment_uid = client.deployments.get_uid(created_deployment)

# すでにデプロイ済みの場合、下記コメントをはずして手動でdeployment_uidを設定する

# deployment_uid = "d2748d63-7b21-49a1-8533-956b421498ca"

# deployment_uidの確認
print(deployment_uid)
```

#### 結果

```
d2748d63-7b21-49a1-8533-956b421498ca
```

# 3. 予測
最後のステップは予測です。今は、たまたま同じnotebookにありますが、普通は別の場所から呼ばれることになります。
その場合は、上で説明した``deployment_uid``の設定に注意するようにして下さい。

## 3.1 検証用データの取得
検証用データは、学習時同様、pytorchのライブラリを使って取得することにします。Watson StudioのJupyterでは、pytorchvisionが含まれていないので、追加導入する必要があります。ただ、この時にバージョンに注意しないと、import時にエラーが起きるようです。
下記のコードが動作確認されたものとなります。

```py3
!pip install torchvision==0.8.1 | tail -n 1
```

```py3
import torch
import torchvision
import torchvision.transforms as transforms
```

念のため、導入済みのバージョンを確認してみます。

```py3
print(torch.__version__)
print(torchvision.__version__)
```

```
1.7.0
0.8.1
```

それでは、実際にデータを読み込んでみましょう。以下のコードは[PyTorch 学習メモ (Karasと同じモデルを作ってみた)](https://qiita.com/makaishi2/items/3676d216fe9b34b63430)とまったく同じものなので、細かい解説は省略します。

```py3
# 分類先クラス名
classes = ['plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

# 分類クラス数
num_classes = len(classes)

# 1回の学習で何枚の画像を使うか
batch_size = 128

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False, num_workers=2)
```

## 3.2 検証用データの組み立てと、イメージ表示
次の読み込んだデータから、検証用の入力データ100件分を組み立てます。
ポイントは、元のデータ形式である、LataLoaderクラスのインスタンスから、ループを回しながら、「入力データ」と「正解ラベル」のセットを取り出し、その結果をリストでつないで、2つの変数``values``と``labels``を作っている点です。
入力データに関しては、元々はTensor形式だったものを、numpy -> list形式に変換します。
また、この処理をしている最中にせっかくなので、読み込んだ入力データをイメージ表示もしています。
具体的なコードは以下のようになります。

```py3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display

N = 100

values = []
labels = []

plt.figure(figsize=(15, 15))

for i in range(N):
    # テスト用データの組み立て
    image, label = testset[i]
    labels.append(label)
    xdata = image.numpy().tolist()
    values .append(xdata)
    
    # データの画面表示 (予測とは無関係。ついでにやっただけ)
    ax = plt.subplot(10, 10, i + 1)
    img = np.transpose(image.numpy(), (1, 2, 0))
    img2 = (img + 1)/2 
    plt.imshow(img2)
    ax.set_title(classes[label], fontsize=10)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

plt.show()
```

イメージはこんな形で出力されるはずです。(下記は一部)

![スクリーンショット 2021-01-12 16.43.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/b49dc2dd-5346-9108-7a6e-fbd766c1aaa4.png)

最後に、今、組み立てた変数``values``を使って、最終的にAPI呼び出しの引数となる変数``scoring_payload``を組み立てます。この書式も、scikit-learnの時とまったく同じです。

```py3
# 入力用パラメータの組み立て
scoring_payload = {"input_data": [{"values": values}]}
```

## 3.3 予測用APIの呼び出し
これで、予測のための準備はすべて終わりました。あとは、ライブラリを使ってAPI呼び出しをします。
具体的なコードは下記になります。

```py3
predictions = client.deployments.score(deployment_uid, scoring_payload)
```

予測結果はPyTorchの場合、下のように、各モデルの確信度の配列になります。

```py3
print(predictions)
```

```
{'predictions': [{'values': [[-4.940670013427734, -4.821472644805908, -3.138746500015259, 4.834586143493652, -8.361305236816406, 3.5139641761779785, 0.46214526891708374, -5.3571882247924805, -2.8474788665771484, -5.455841541290283], [2.7644705772399902, 2.6016273498535156, -15.107336044311523, -17.771255493164062, -18.063720703125, -23.072168350219727, -12.652151107788086, -24.18853759765625, 16.923925399780273, -3.93799090385437], [3.2324273586273193, 0.6664918661117554, -9.696178436279297, -7.741365432739258, -7.4524688720703125, -11.031821250915527, -8.739614486694336, -9.531129837036133, 7.229529857635498, -0.4289969205856323], (以下略)
```

``np.argmax``関数を使って、どの要素が最大化を調べることで、予測クラスがわかります。具体的なコードは次のとおりです。(比較のため、正解ラベルも表示してみました)

```py3
w1 = predictions['predictions'][0]['values']
w2 = np.array([np.argmax(x) for x in w1])
print('予測: ',w2)
print('正解: ', np.array(labels))
```

```
予測:  [3 8 8 0 6 6 1 4 3 9 0 9 5 7 9 6 5 7 8 6 7 0 4 9 4 4 4 0 9 6 6 5 4 5 9 8 4
 9 9 5 4 6 5 6 0 9 3 9 7 4 9 8 7 3 8 8 5 3 3 5 7 5 6 3 6 2 1 2 3 7 0 6 8 8
 0 2 0 3 3 8 8 1 1 7 2 5 2 4 8 9 0 6 8 6 4 6 6 0 0 7]
正解:  [3 8 8 0 6 6 1 6 3 1 0 9 5 7 9 8 5 7 8 6 7 0 4 9 5 2 4 0 9 6 6 5 4 5 9 2 4
 1 9 5 4 6 5 6 0 9 3 9 7 6 9 8 0 3 8 8 7 7 4 6 7 3 6 3 6 2 1 2 3 7 2 6 8 8
 0 2 9 3 3 8 8 1 1 7 2 5 2 7 8 9 0 3 8 6 4 6 6 0 0 7]
```

ぱっと見、8割程度で正解が得られていそうなことがわかります。
最後に混同行列表示もしてみます。
ここは、コードでなく結果のみ載せることにします。

![スクリーンショット 2021-01-12 16.56.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206130/55385cd1-95dd-59ab-cf98-259a3e49efe3.png)

対角線のマス目(=正解)に多くの件数が集まっていて、そこそこの精度が出ていることがわかりました。

## 参考記事
[Use PyTorch to predict handwritten digits](https://github.com/IBM/pytorch-on-watson-studio/blob/master/notebooks/use-pytorch-to-predict-handwritten-digits.ipynb)