Vertex AIを使って画像セグメンテーションを実施するまで

* GCP の Vertex AI で画像セグメンテーションを行う場合、カスタムトレーニングが必須となっています。(2022-02-20)
* 手順を整理しつつ、Vertex AI で画像セグメンテーションを実施するまでの手順をまとめたいと思います。

目次
[まずはチュートリアルでカスタムトレーニングを学ぶ](#まずはチュートリアルでカスタムトレーニングを学ぶ)
[画像セグメンテーションの実装とVertexAIでのトレーニング](#画像セグメンテーションの実装とVertexAIでのトレーニング)

## まずはチュートリアルでカスタムトレーニングを学ぶ

* こちらのGCP公式ドキュメントを参考にして、カスタムトレーニングの流れを掴みます。
https://cloud.google.com/vertex-ai/docs/training/custom-training
* 今回 webapp/ は使いませんでした。
（内容としてはウェブ画面から予測する画像を選択して予測結果を受け取るものなので無くても問題ありません。）

    > 上記GCP公式ドキュメントより引用
    > **hello-custom-sample ディレクトリには 4 つの項目があります。**
    > **trainer/:** 花の分類モデルをトレーニングするための TensorFlow Keras コードのディレクトリ。
    > **setup.py:** Vertex AI が使用できる Python ソース配布に trainer/ ディレクトリをパッケージ化するための構成ファイル。
    > **function/:** ウェブブラウザから予測リクエストを受信し、前処理、Vertex AI への送信、予測レスポンスの処理を行ってブラウザに送り返す Cloud Functions の関数の Python コードのディレクトリ。
    > **webapp/:** Vertex AI から花の分類予測を取得するウェブアプリのコードとマークアップを含むディレクトリ。

* Cloud Functions に関数(function/main.py)をデプロイする際にチュートリアルのままだとエラーが発生したため、「function/requirements.txt」に「itsdangerous==2.0.1」を追加しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/2050d021-a6b4-4387-057f-f89c3a0cd626.png)

* Cloud Build の設定で有効化しないとデプロイ出来なかったので有効化が必要なところは有効化します。
（一応必要そうなところを黄色で囲いましたが、他にも必要なものがあるかも？）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/6cfa615f-e4ae-f511-3891-a535166f928e.png)

* postman で上記デプロイした関数にリクエストを行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/037474e2-c812-905b-fdfc-a2907cd78916.png)
リクエストした画像に対する予測結果がレスポンスに含まれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/63f54a23-db2c-3924-b5eb-7424b7cf429c.png)
* 関数をデプロイせず GCP のモデルのエンドポイントに直接リクエストを投げる方法も試してみましたが、
画像を tf.io.decode_jpeg() で下記のような多次元配列にした上で JSON 化してリクエストする必要がありそうだったので素直にチュートリアルに従いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/a5966b6b-4c71-1c09-1338-a545c6c39914.png)

## 画像セグメンテーションの実装とVertexAIでのトレーニング

1. 開発環境を用意する
1. コードを用意する
1. ローカル環境でのトレーニングの動作確認
1. 画像を google cloud storage に送る
1. google cloud storage に資材を送り、カスタムトレーニングを実行する
1. カスタムトレーニングで作成されたモデルを使って予測を行う

* Vertex AI でカスタムトレーニングをする際はコンテナを用意しますが、今回はビルド済みのコンテナを使いました。
ビルド済みコンテナのモデルフレームワークは下記から選択可能となっています。
（隠れていますがバージョンの指定もあります。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/e3fcaf08-fe1f-84e2-4234-68e1dad52672.png)
* PyTorch + segmentation-models-pytorch を使用しました。
* [segmentation-models-pytorch](https://github.com/qubvel/segmentation_models.pytorch) の詳細についてはこちら。
* サンプルコードで画像拡張ライブラリの Albumentations を使っているので、そちらも使用しています。
（画像に加工を加えて、少ない画像でもデータバリエーションを増やせるらしいです。）

#### ＊開発環境を用意する
| 開発環境 |
| ---- |
|  [PyCharm 2021.3.2 (Community Edition)](https://www.jetbrains.com/ja-jp/pycharm/download/)  |
|  [Python 3.7.9](https://www.python.org/downloads/)  |

#### ＊コードを用意する
* プロジェクトの構成
基本的に VertexAI のチュートリアルに沿った構成にします。
[Project]
　┣[trainer]
　┃　┣[data]（画像データが保存されています。ローカル用です。）
　┃　┣__init__.py
　┃　┗task.py
　┗setup.py

* setup.py：プロジェクト名やライブラリの依存関係を入力します。
```python
from setuptools import find_packages, setup

REQUIRED_PACKAGES = ['segmentation-models-pytorch==0.2.1',
                     'albumentations==0.4.5',
                     'matplotlib==3.5.1']
setup(
    name='segmentation-custom-training',
    version='0.1',
    install_requires=REQUIRED_PACKAGES,
    packages=find_packages(),
    description='segmentation test'
    )
```
* task.py：トレーニングコードを記載します。
    * 基本は[サンプルコード](https://github.com/qubvel/segmentation_models.pytorch/blob/master/examples/cars%20segmentation%20(camvid).ipynb)にのっとっていますが、事前にサンプルコードの画像取得処理は実行して、データを取得している状態です。
```python
import cv2
import numpy as np
import os
from torch.utils.data import DataLoader
from torch.utils.data import Dataset as BaseDataset
import torch
import segmentation_models_pytorch as smp
import albumentations as albu

# 変更点：google cloud storage のディレクトを指定します。
# DATA_DIR = './data/CamVid/'
DATA_DIR = '/gcs/[BUCKET]/custom-t-data'
output_directory = '/gcs/[BUCKET]/custom-t/output/'

x_train_dir = os.path.join(DATA_DIR, 'train')
y_train_dir = os.path.join(DATA_DIR, 'trainannot')
x_valid_dir = os.path.join(DATA_DIR, 'val')
y_valid_dir = os.path.join(DATA_DIR, 'valannot')
# x_test_dir = os.path.join(DATA_DIR, 'test')
# y_test_dir = os.path.join(DATA_DIR, 'testannot')

class Dataset(BaseDataset):
    """CamVid Dataset. Read images, apply augmentation and preprocessing transformations.
    Args:
        images_dir (str): path to images folder
        masks_dir (str): path to segmentation masks folder
        class_values (list): values of classes to extract from segmentation mask
        augmentation (albumentations.Compose): data transfromation pipeline
            (e.g. flip, scale, etc.)
        preprocessing (albumentations.Compose): data preprocessing
            (e.g. noralization, shape manipulation, etc.)
    """
    CLASSES = ['sky', 'building', 'pole', 'road', 'pavement',
               'tree', 'signsymbol', 'fence', 'car',
               'pedestrian', 'bicyclist', 'unlabelled']

    def __init__(
            self,
            images_dir,
            masks_dir,
            classes=None,
            augmentation=None,
            preprocessing=None,
    ):
        self.ids = os.listdir(images_dir)
        self.images_fps = [os.path.join(images_dir, image_id) for image_id in self.ids]
        self.masks_fps = [os.path.join(masks_dir, image_id) for image_id in self.ids]

        # convert str names to class values on masks
        self.class_values = [self.CLASSES.index(cls.lower()) for cls in classes]

        self.augmentation = augmentation
        self.preprocessing = preprocessing

    def __getitem__(self, i):

        # read data
        image = cv2.imread(self.images_fps[i])
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        mask = cv2.imread(self.masks_fps[i], 0)

        # extract certain classes from mask (e.g. cars)
        masks = [(mask == v) for v in self.class_values]
        mask = np.stack(masks, axis=-1).astype('float')

        # apply augmentations
        if self.augmentation:
            sample = self.augmentation(image=image, mask=mask)
            image, mask = sample['image'], sample['mask']

        # apply preprocessing
        if self.preprocessing:
            sample = self.preprocessing(image=image, mask=mask)
            image, mask = sample['image'], sample['mask']

        return image, mask

    def __len__(self):
        return len(self.ids)

def get_training_augmentation():
    train_transform = [
        albu.HorizontalFlip(p=0.5),

        albu.ShiftScaleRotate(scale_limit=0.5, rotate_limit=0, shift_limit=0.1, p=1, border_mode=0),

        albu.PadIfNeeded(min_height=320, min_width=320, always_apply=True, border_mode=0),
        albu.RandomCrop(height=320, width=320, always_apply=True),

        albu.IAAAdditiveGaussianNoise(p=0.2),
        albu.IAAPerspective(p=0.5),

        albu.OneOf(
            [
                albu.CLAHE(p=1),
                albu.RandomBrightness(p=1),
                albu.RandomGamma(p=1),
            ],
            p=0.9,
        ),
        albu.OneOf(
            [
                albu.IAASharpen(p=1),
                albu.Blur(blur_limit=3, p=1),
                albu.MotionBlur(blur_limit=3, p=1),
            ],
            p=0.9,
        ),
        albu.OneOf(
            [
                albu.RandomContrast(p=1),
                albu.HueSaturationValue(p=1),
            ],
            p=0.9,
        ),
    ]
    return albu.Compose(train_transform)

def get_validation_augmentation():
    """Add paddings to make image shape divisible by 32"""
    test_transform = [
        albu.PadIfNeeded(384, 480)
    ]
    return albu.Compose(test_transform)

def to_tensor(x, **kwargs):
    return x.transpose(2, 0, 1).astype('float32')

def get_preprocessing(preprocessing_fn):
    """Construct preprocessing transform
    Args:
        preprocessing_fn (callbale): data normalization function
            (can be specific for each pretrained neural network)
    Return:
        transform: albumentations.Compose
    """
    _transform = [
        albu.Lambda(image=preprocessing_fn),
        albu.Lambda(image=to_tensor, mask=to_tensor),
    ]
    return albu.Compose(_transform)

#### Visualize resulted augmented images and masks
augmented_dataset = Dataset(
    x_train_dir,
    y_train_dir,
    augmentation=get_training_augmentation(),
    classes=['car'],
)

ENCODER = 'se_resnext50_32x4d'
ENCODER_WEIGHTS = 'imagenet'
CLASSES = ['car']
ACTIVATION = 'sigmoid' # could be None for logits or 'softmax2d' for multiclass segmentation
# 変更点：ここは VertexAI で GPU を使う場合は cuda のままですが、
# ローカルPCで動作確認する場合など、GPU が無い環境では cpu を選択します。
DEVICE = 'cuda'
# DEVICE = 'cpu'

# create segmentation model with pretrained encoder
model = smp.FPN(
    encoder_name=ENCODER,
    encoder_weights=ENCODER_WEIGHTS,
    classes=len(CLASSES),
    activation=ACTIVATION,
)

preprocessing_fn = smp.encoders.get_preprocessing_fn(ENCODER, ENCODER_WEIGHTS)

train_dataset = Dataset(
    x_train_dir,
    y_train_dir,
    augmentation=get_training_augmentation(),
    preprocessing=get_preprocessing(preprocessing_fn),
    classes=CLASSES,
)

valid_dataset = Dataset(
    x_valid_dir,
    y_valid_dir,
    augmentation=get_validation_augmentation(),
    preprocessing=get_preprocessing(preprocessing_fn),
    classes=CLASSES,
)

train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=0)
valid_loader = DataLoader(valid_dataset, batch_size=1, shuffle=False, num_workers=0)
# Dice/F1 score - https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient
# IoU/Jaccard score - https://en.wikipedia.org/wiki/Jaccard_index
loss = smp.utils.losses.DiceLoss()
metrics = [
    smp.utils.metrics.IoU(threshold=0.5),
]

optimizer = torch.optim.Adam([
    dict(params=model.parameters(), lr=0.0001),
])

# create epoch runners
# it is a simple loop of iterating over dataloader`s samples
train_epoch = smp.utils.train.TrainEpoch(
    model,
    loss=loss,
    metrics=metrics,
    optimizer=optimizer,
    device=DEVICE,
    verbose=True,
)

valid_epoch = smp.utils.train.ValidEpoch(
    model,
    loss=loss,
    metrics=metrics,
    device=DEVICE,
    verbose=True,
)

# train model for 40 epochs
max_score = 0
for i in range(0, 40):
    print('\nEpoch: {}'.format(i))

    train_logs = train_epoch.run(train_loader)
    valid_logs = valid_epoch.run(valid_loader)

    # do something (save model, change lr, etc.)
    if max_score < valid_logs['iou_score']:
        max_score = valid_logs['iou_score']
        # 変更点：モデルを CPU 環境で使う場合は cpu にして保存して、トレーニング再開前に cuda に戻します。
        # 常に cuda 環境を使う場合は特に変更する必要はないと思います。
        model = model.to('cpu')
        torch.save(model.state_dict(), output_directory + 'model.pth')
        model = model.to('cuda')
        print('Model saved!')

    if i == 25:
        optimizer.param_groups[0]['lr'] = 1e-5
        print('Decrease decoder learning rate to 1e-5!')
```

#### ＊ローカル環境でのトレーニングの動作確認
* 画像データを用意します。
サンプルコードにある方法でダウンロードするか、
[SegNet-Tutorial/tree/master/CamVid](https://github.com/alexgkendall/SegNet-Tutorial/tree/master/CamVid) から直接ダウンロードします。
* 「画像ディレクトリ」「トーレニングで使うDEVICE」「保存時のDEVICE」は自分の環境にあわせて変更して task.py を実行します。
* 下記のようになれば実行に成功しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/2f17171a-6483-7b89-3c84-cc7a21e07a4a.png)


#### ＊画像を google cloud storage に送る
* トレーニング用の画像を google cloud storage にアップロードします。
* 保存先はコードから参照可能な場所とします。（例：DATA_DIR = '/gcs/[BUCKET]/custom-t-data' に合わせる）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/5fbba957-add9-6588-ed18-9323e44b4821.png)

#### ＊google cloud storage に資材を送り、カスタムトレーニングを実行する
続いて google cloud storage に資材を送ります。
ここは VertexAI のチュートリアルと同じ操作をします。
1. まずは必要なファイルを cloud shell からアップロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/7e7aec5a-72bf-2002-4002-2ed18556e394.png)
1. アップロードしたフォルダに移動して、以下コマンドでtarball形式でフォーマットします。
``` python3 setup.py sdist --formats=gztar ```

1. google cloud storage にフォーマットしたファイルをコピーして、コピーされていることを確認します。
``` gsutil cp dist/segmentation-custom-training-0.1.tar.gz   gs://[BUCKET]/custom-t/training/ ```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/84514bbf-3196-d602-a880-d1e184f4cc69.png)
1. [VertexAI]━[トレーニング]━[作成] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/a6f3c5d1-c1bb-569d-56b8-90fdaa5c2074.png)
1. ●Dataset：管理されているデータセットなし
●Model training method：カスタムトレーニング
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/187e1ac6-0cb3-5de0-8249-3abd4b788e88.png)
1. モデル名を入力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/8406e945-ff84-a74c-da45-b57fc18ca448.png)
1. ●ビルド済みコンテナを選択
●モデルフレームワーク：PyTorch
●モデルフレームワークのバージョン：1.10
●パッケージの場所：生成したソースコード配布物を指定（.tar.gz）
●Pythonモジュール：エントリ ポイント（最初に起動する.py）を指定します。
今回は trainer/task.py しか実行ファイルは無いので特に考えず trainer.task を指定します。
●Model output directory：トレーニング後のモデル保存先を指定します。
今回はトレーニングコード中でパスを指定しているため未入力です。
●引数：トレーニングプログラムに引数を入力する必要があば入力します。今回は未入力です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/47f0e574-f3c1-932f-bfb1-29aa8d94ce49.png)
1. トレーニング時のパラメータを複数試して一番良い結果のモデルを保存します。
今回は未選択のままにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/48f91cbf-6bbe-1689-7be5-4feac761f8dd.png)
1. ●Machine type：トレーニングで使用するマシンスペックを指定します。
●Accelerator type：トレーニングで使用するGPUを指定します。
ここで指定しない＆プログラム中で"cuda"を指定しているとトレーニング開始時にエラーになります。
●Accelerator count：GPUの数
●Disk type：使用するディスクの種類。デフォルトSSDなのでそのまま。
●Disk size：使用するディスクの容量。デフォルト100(GB)なのでそのまま。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/1d5b91e0-f1b4-60a5-a7fe-a65eebb7a40a.png)
1. トレーニング後の予測で使用するコンテナを作成します。
ビルド済みコンテナに「PyTorch」がなかったため「No prediction container」を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/df8eed25-467b-906c-a046-c81d441debeb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/e85be2ab-86cf-4324-b430-eefeb048a9ee.png)
1. 「トレーニングを開始」します。
「ログエクスプローラ」で進捗を確認出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/d0d5ebcb-f216-260d-6693-7d27f5ee2283.png)
1. トレーニングが完了すると指定した場所にモデルファイルが生成されているので、自身のローカルにダウンロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/6a5d6762-42b6-47ea-e200-f382d54fc20a.png)

#### ＊カスタムトレーニングで作成されたモデルを使って予測を行う
* test.py：予測用のコードを記載します。task.py と同じ階層に用意しています。
* こちらのコードも[サンプル](https://github.com/qubvel/segmentation_models.pytorch/blob/master/examples/cars%20segmentation%20(camvid).ipynb)を一部変更して使っています。
* コードを用意したら test.py を実行します。
```python
# ライブラリ読み込み
import cv2
import numpy as np
import os
from torch.utils.data import DataLoader
from torch.utils.data import Dataset as BaseDataset
import torch
import segmentation_models_pytorch as smp
import albumentations as albu
import matplotlib.pyplot as plt

# test 画像を使って予測します。
DATA_DIR = './data/CamVid/'
x_test_dir = os.path.join(DATA_DIR, 'test')
y_test_dir = os.path.join(DATA_DIR, 'testannot')

# helper function for data visualization
def visualize(**images):
    """PLot images in one row."""
    n = len(images)
    plt.figure(figsize=(16, 5))
    for i, (name, image) in enumerate(images.items()):
        plt.subplot(1, n, i + 1)
        plt.xticks([])
        plt.yticks([])
        plt.title(' '.join(name.split('_')).title())
        plt.imshow(image)
    plt.show()

class Dataset(BaseDataset):
    """CamVid Dataset. Read images, apply augmentation and preprocessing transformations.

    Args:
        images_dir (str): path to images folder
        masks_dir (str): path to segmentation masks folder
        class_values (list): values of classes to extract from segmentation mask
        augmentation (albumentations.Compose): data transfromation pipeline
            (e.g. flip, scale, etc.)
        preprocessing (albumentations.Compose): data preprocessing
            (e.g. noralization, shape manipulation, etc.)

    """

    CLASSES = ['sky', 'building', 'pole', 'road', 'pavement',
               'tree', 'signsymbol', 'fence', 'car',
               'pedestrian', 'bicyclist', 'unlabelled']

    def __init__(
            self,
            images_dir,
            masks_dir,
            classes=None,
            augmentation=None,
            preprocessing=None,
    ):
        self.ids = os.listdir(images_dir)
        self.images_fps = [os.path.join(images_dir, image_id) for image_id in self.ids]
        self.masks_fps = [os.path.join(masks_dir, image_id) for image_id in self.ids]

        # convert str names to class values on masks
        self.class_values = [self.CLASSES.index(cls.lower()) for cls in classes]

        self.augmentation = augmentation
        self.preprocessing = preprocessing

    def __getitem__(self, i):

        # read data
        image = cv2.imread(self.images_fps[i])
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        mask = cv2.imread(self.masks_fps[i], 0)

        # extract certain classes from mask (e.g. cars)
        masks = [(mask == v) for v in self.class_values]
        mask = np.stack(masks, axis=-1).astype('float')

        # apply augmentations
        if self.augmentation:
            sample = self.augmentation(image=image, mask=mask)
            image, mask = sample['image'], sample['mask']

        # apply preprocessing
        if self.preprocessing:
            sample = self.preprocessing(image=image, mask=mask)
            image, mask = sample['image'], sample['mask']

        return image, mask

    def __len__(self):
        return len(self.ids)

def get_validation_augmentation():
    """Add paddings to make image shape divisible by 32"""
    test_transform = [
        albu.PadIfNeeded(384, 480)
    ]
    return albu.Compose(test_transform)


def to_tensor(x, **kwargs):
    return x.transpose(2, 0, 1).astype('float32')


def get_preprocessing(preprocessing_fn):
    """Construct preprocessing transform

    Args:
        preprocessing_fn (callbale): data normalization function
            (can be specific for each pretrained neural network)
    Return:
        transform: albumentations.Compose

    """

    _transform = [
        albu.Lambda(image=preprocessing_fn),
        albu.Lambda(image=to_tensor, mask=to_tensor),
    ]
    return albu.Compose(_transform)

ENCODER = 'se_resnext50_32x4d'
ENCODER_WEIGHTS = 'imagenet'
CLASSES = ['car']
ACTIVATION = 'sigmoid' # could be None for logits or 'softmax2d' for multiclass segmentation
# cpu 環境しか持っていないため cpu にしています。
# モデル作成時に cpu を指定していたため cuda にするとエラーになるかもしれません（未確認）
# DEVICE = 'cuda'
DEVICE = 'cpu'

# create segmentation model with pretrained encoder
model = smp.FPN(
    encoder_name=ENCODER,
    encoder_weights=ENCODER_WEIGHTS,
    classes=len(CLASSES),
    activation=ACTIVATION,
)

preprocessing_fn = smp.encoders.get_preprocessing_fn(ENCODER, ENCODER_WEIGHTS)

loss = smp.utils.losses.DiceLoss()
metrics = [
    smp.utils.metrics.IoU(threshold=0.5),
]

# モデルで推論
# カスタムトレーニングで作成された model ファイルを指定します。
model.load_state_dict(torch.load("./data/custom-t_output_model.pth"))

# create test dataset
test_dataset = Dataset(
    x_test_dir,
    y_test_dir,
    augmentation=get_validation_augmentation(),
    preprocessing=get_preprocessing(preprocessing_fn),
    classes=CLASSES,
)
test_dataloader = DataLoader(test_dataset)

# evaluate model on test set
test_epoch = smp.utils.train.ValidEpoch(
    model=model,
    loss=loss,
    metrics=metrics,
    device=DEVICE,
)

logs = test_epoch.run(test_dataloader)

# test dataset without transformations for image visualization
test_dataset_vis = Dataset(
    x_test_dir, y_test_dir,
    classes=CLASSES,
)

for i in range(5):
    n = np.random.choice(len(test_dataset))

    image_vis = test_dataset_vis[n][0].astype('uint8')
    image, gt_mask = test_dataset[n]

    gt_mask = gt_mask.squeeze()

    x_tensor = torch.from_numpy(image).to(DEVICE).unsqueeze(0)
    pr_mask = model.predict(x_tensor)
    pr_mask = (pr_mask.squeeze().cpu().numpy().round())

    visualize(
        image=image_vis,
        ground_truth_mask=gt_mask,
        predicted_mask=pr_mask
    )

```

* 実行すると予測が始まります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/0ff0e985-1be1-5f13-7bba-9c644a7978d5.png)
* 一番右が予測した画像なのでトレーニングは上手く出来ていそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424166/09d0e0a1-6b4f-0139-af2c-286b82407497.png)

## 今後の課題
* コード内のパラメータについてチュートリアル通りに作ったので理解出来ていない部分が多いためいろいろ変更してみる。
* GCP で完結していないためGCPで予測出来るようにしてみる。（予測用コンテナを作る必要有り？）
* トレーニング用の画像は既存のものを使ったので、自身で作成出来るようにする。
* モデルの品質は出力可能か？（VertexAI の autoML だと出力された。）