さくらインターネットのGPUサービスDOKでtorchvision（画像分類）を試す

[DOK](https://www.sakura.ad.jp/koukaryoku-dok/)はコンテナー型のGPUサービスで、NVIDIA V100とかH100を実行時間課金で利用できるサービスです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/98a7fac2-c363-088f-8072-0a6b65395fe2.png)


[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） | さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)

今回はこのDOKで、[torchvision（学習済みモデルを使った画像分類）](https://pytorch.org/vision/stable/index.html)を試してみました。

## 実行までのステップ

1. さくらのクラウドのアカウントの用意（必須）
2. Dockerイメージのビルド環境（Linuxなど）を用意
3. Dockerイメージのビルド
4. DOKにイメージをアップロード
5. DOKでコンテナーを起動・実行

## 1. さくらのクラウドのアカウントの用意

DOKはさくらのクラウドに紐付いたサービスなので、さくらのクラウドのアカウント必須です。

[クラウドサーバーはIaaS型のさくらのクラウド](https://cloud.sakura.ad.jp/)

### コンテナレジストリの作成

さくらのクラウドにログインしたら `さくらのクラウド` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e247a30c-bbc7-fb7e-136d-71ee2a795533.png)


左側のメニューの `LAB` の中にある `コンテナレジストリ` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/25d041f2-9039-fec3-77c6-88edf5afa936.png)


`追加` を押して、コンテナレジストリを作成します。最低限、以下の入力が必要です。

| 項目                 | 設定                                                                              |
| -------------------- | --------------------------------------------------------------------------------- |
| 名前                 | 分かりやすい、任意の名前を入力してください                                        |
| コンテナレジストリ名 | ドメイン名に使われます。 `EXAMPLE.sakuracr.jp` というコンテナレジストリになります |
| 公開設定             | 非公開で良いかと思います                                                          |

#### ユーザーの作成

コンテナレジストリにアクセスできるユーザーを作成します。作成したコンテナレジストリをダブルクリックして、詳細を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/b9f5ef9d-ebef-c24e-de89-f63c5ce5d443.png)


`ユーザー` タブを選択して、`追加` を押します。

IDとパスワードを設定して、権限は `All` にして作成してください。

## 2. Dockerイメージのビルド環境（Linuxなど）を用意

WindowsのWSL2でできるかは分かりませんが、少なくともmacOS（M1など）ではDockerイメージをビルドできなかったので、別途用意が必要です。これは適当なVPSサーバーや自宅サーバ、さくらのクラウドでインスタンスを立ち上げるでもOKです。

以下はスペックの例です。

- コア  
2
- メモリ  
4GB
- OS  
Ubuntu Server 22.04.4 LTS 64bit
- ディスク  
100GB

## 3. Dockerイメージのビルド

作業フォルダを作成します。

```bash
mkdir torchvision
cd torchvision
```

後はこの `torchvision` ディレクトリの中で処理を書いていきます。

### Dockerfileの作成

Dockerfileを作成します。内容は以下の通りです。

```dockerfile
FROM nvidia/cuda:12.5.1-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive

# 必要なパッケージのインストール
# /appはアプリのディレクトリ、/opt/artifactはアウトプット先のディレクトリ
RUN apt-get update && \
    apt-get install -y \
        git \
        git-lfs \
        python3 \
        python3-pip \
      && \
    mkdir /app /opt/artifact && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# VALL-E Xのリポジトリをクローン
WORKDIR /app
# git-lfsのインストール
# 依存ライブラリのインストール
COPY requirements.txt /app/
RUN pip install -r requirements.txt
# 出力データをオブジェクトストレージにアップロードするためのライブラリ
RUN pip cache purge

# 実行スクリプト（後で作成）
COPY runner.py /app/
# Dockerコンテナー起動時に実行するスクリプト（後で作成）
COPY docker-entrypoint.sh /
# 実行権限を付与
RUN chmod +x /docker-entrypoint.sh /

WORKDIR /
# Dockerコンテナー起動時に実行するスクリプトを指定して実行
CMD ["/bin/bash", "/docker-entrypoint.sh"]
```

### docker-entrypoint.shの作成

docker-entrypoint.shは、Dockerコンテナー起動時に実行するスクリプトです。内容は以下の通りです。

```bash
#!/bin/bash

set -ue
shopt -s nullglob

export TZ=${TZ:-Asia/Tokyo}

# アウトプット先ディレクトリ（自動付与） /opt/artifact固定です
if [ -z "${SAKURA_ARTIFACT_DIR:-}" ]; then
  echo "Environment variable SAKURA_ARTIFACT_DIR is not set" >&2
  exit 1
fi

# DOKのタスクID（自動付与）
if [ -z "${SAKURA_TASK_ID:-}" ]; then
  echo "Environment variable SAKURA_TASK_ID is not set" >&2
  exit 1
fi

# 分析対象のURL
if [ -z "${URL:-}" ]; then
  echo "Environment variable URL is not set" >&2
  exit 1
fi

# S3_はすべてboto3用の環境変数です
pushd /app
  python3 runner.py \
	  --id="${SAKURA_TASK_ID}" \
	  --output="${SAKURA_ARTIFACT_DIR}" \
	  --url="${URL}" \
	  --s3-bucket="${S3_BUCKET:-}" \
	  --s3-endpoint="${S3_ENDPOINT:-}" \
	  --s3-secret="${S3_SECRET:-}" \
	  --s3-token="${S3_TOKEN:-}"
popd
```

### requirements.txtの作成

Pythonライブラリをインストールするために、requirements.txtを作成します。

```txt
requests
torch
torchvision
boto3
```

### runner.pyの作成

runner.pyは実際に実行するスクリプトです。内容は以下の通りです。

#### ライブラリの読み込み

最初にライブラリを読み込みます。

```python
import json
from pathlib import Path
import os
import requests
import io
import json
import argparse
import boto3
import torch
from torch.nn import functional as F
import torchvision
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from torchvision.datasets.utils import download_url
from PIL import Image
```

#### 引数のパース

実行時に渡される引数をパースします。

```python
arg_parser = argparse.ArgumentParser()

arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)
arg_parser.add_argument(
    '--url',
    help='分析対象のURLを指定します。複数指定可能（カンマ区切り）',
)
arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)
arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = arg_parser.parse_args()
```

#### GPU判定用関数の作成

GPUが利用するかどうか判定する関数 `get_device` を作成します。

```python
def get_device(use_gpu):
        # GPUを使うかどうかを判定し、使えるならばGPUを返す
        if use_gpu and torch.cuda.is_available():
                # 計算の再現性を担保するために設定を行う
                torch.backends.cudnn.deterministic = True
                return torch.device("cuda")    # GPUデバイスを返す
        else:
                return torch.device("cpu")    # CPUデバイスを返す
```

#### クラス名の判定用関数の作成

クラス名を判定するのに利用するJSONファイルを取得、保存する関数 `get_classes` を作成します。

```python
def get_classes():
    # クラス名のリストを保持するファイルが存在するか確認
    if not Path("data/imagenet_class_index.json").exists():
        # ファイルが存在しない場合、ダウンロードして保存
        download_url("https://git.io/JebAs", "data", "imagenet_class_index.json")

    # クラス一覧をJSONファイルから読み込む
    with open("data/imagenet_class_index.json") as f:
        data = json.load(f)
        class_names = [x["ja"] for x in data]    # 日本語のクラス名をリスト化
    return class_names
```

#### メイン処理

残りのメイン処理は `main` 関数に実装します。

```python
def main():
    # この中に実装します

if __name__ == "__main__":
    main()    # メイン関数を実行
```

#### モデルの読み込み

必要なモデルをダウンロードして読み込みます。

```python
# デバイス（GPUまたはCPU）を取得する
device = get_device(use_gpu=True)

# ResNet50モデルを読み込み、指定したデバイスに転送する
model = torchvision.models.resnet50(pretrained=True).to(device)
```

#### 画像を前処理するための変換処理を定義する

画像を前処理するための変換処理を `transforms.Compose` で定義します。

```python
# 画像を前処理するための変換処理を定義
transform = transforms.Compose([
    transforms.Resize(256),    # 画像を256x256にリサイズする
    transforms.CenterCrop(224),    # 画像の中心から224x224の部分を切り抜く
    transforms.ToTensor(),    # 画像をテンソル形式に変換する
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
    ),    # 各ピクセル値を標準化（正規化）する
])
```

#### 画像の分だけ処理

引数の `url` を `,` で分割して、その分だけ処理を行います。

```python
# 環境変数URLを取得、,で分割
urls = args.url.split(',')
results = {}
# URLの数だけループ
for url in urls:
    # 以下はこの中
```

#### 画像のダウンロード

画像を取得して、 `RGB` に変換します。

```python
results[url] = []
# 指定されたURLから画像を取得し、RGB形式に変換する
res = requests.get(url)
# resが200番でない場合はスキップ
if res.status_code != 200:
    continue
img = Image.open(io.BytesIO(res.content)).convert("RGB")
```

#### 画像に前処理を適用

前処理を適用して、モデルに入力できる形に変換します。

```python
# 画像に前処理を適用
inputs = transform(img)

# バッチ処理のために次元を追加し、指定デバイスに転送する
inputs = inputs.unsqueeze(0).to(device)
```

#### 予測を行う

モデルに入力データを渡し、予測を行います。

```python
# モデルを評価モードに設定（推論時の設定）
model.eval()

# モデルに入力データを通して予測を行う
outputs = model(inputs)

# 出力の確率を計算し、クラスごとにソフトマックスを適用する
batch_probs = F.softmax(outputs, dim=1)

# 出力の確率を降順にソートし、対応するクラスのインデックスも取得
batch_probs, batch_indices = batch_probs.sort(dim=1, descending=True)
```

#### クラス名を取得する

クラス名を取得します。クラス名は上位3つの予測結果を利用します。

```python
# クラス名を取得
class_names = get_classes()

# 各入力データに対して、上位3つの予測結果をURLのペアとともにJSON化
for probs, indices in zip(batch_probs, batch_indices):
    for k in range(3):
        results[url].append({"class": class_names[indices[k]], "prob": probs[k].item()})
```

#### 結果を保存

結果を `/opt/artifact` ディレクトリに保存します。

```python
# JSONをファイル出力
file = f'{args.output}/output-{args.id}.json'
with open(file, 'w') as f:
    json.dump(results, f, ensure_ascii=False, indent=2)
```

#### オブジェクトストレージへのアップロード

もし、S3_ではじまる環境変数が設定されていれば、boto3を使って出力内容をアップロードします。

```python
    if args.s3_token and args.s3_secret and args.s3_bucket:
        # S3クライアントの作成
        s3 = boto3.client(
            's3',
            endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
            aws_access_key_id=args.s3_token,
            aws_secret_access_key=args.s3_secret,
        )
        # ファイルアップロード
        s3.upload_file(
            Filename=file,
            Bucket=args.s3_bucket,
            Key=os.path.basename(file),
        )
```

#### 全体のコード

`runner.py` の全体のコードは以下の通りです。

```python
import json
from pathlib import Path
import os
import requests
import io
import json
import argparse
import boto3
import torch
from torch.nn import functional as F
import torchvision
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from torchvision.datasets.utils import download_url
from PIL import Image

arg_parser = argparse.ArgumentParser()

arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)
arg_parser.add_argument(
    '--url',
    help='分析対象のURLを指定します。複数指定可能（カンマ区切り）',
)
arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)
arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = arg_parser.parse_args()

def get_device(use_gpu):
        # GPUを使うかどうかを判定し、使えるならばGPUを返す
        if use_gpu and torch.cuda.is_available():
                # 計算の再現性を担保するために設定を行う
                torch.backends.cudnn.deterministic = True
                return torch.device("cuda")    # GPUデバイスを返す
        else:
                return torch.device("cpu")    # CPUデバイスを返す

def main():
    # デバイス（GPUまたはCPU）を取得する
    device = get_device(use_gpu=True)
    
    # ResNet50モデルを読み込み、指定したデバイスに転送する
    model = torchvision.models.resnet50(pretrained=True).to(device)

    # 画像を前処理するための変換処理を定義
    transform = transforms.Compose([
        transforms.Resize(256),    # 画像を256x256にリサイズする
        transforms.CenterCrop(224),    # 画像の中心から224x224の部分を切り抜く
        transforms.ToTensor(),    # 画像をテンソル形式に変換する
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
        ),    # 各ピクセル値を標準化（正規化）する
    ])

    # 環境変数URLを取得、,で分割
    urls = args.url.split(',')
    results = {}
    # URLの数だけループ
    for url in urls:
        results[url] = []
        # 指定されたURLから画像を取得し、RGB形式に変換する
        res = requests.get(url)
        # resが200番でない場合はスキップ
        if res.status_code != 200:
            continue
        img = Image.open(io.BytesIO(res.content)).convert("RGB")
    
        # 画像に前処理を適用
        inputs = transform(img)
    
        # バッチ処理のために次元を追加し、指定デバイスに転送する
        inputs = inputs.unsqueeze(0).to(device)

        # モデルを評価モードに設定（推論時の設定）
        model.eval()
    
        # モデルに入力データを通して予測を行う
        outputs = model(inputs)
        
        # 出力の確率を計算し、クラスごとにソフトマックスを適用する
        batch_probs = F.softmax(outputs, dim=1)

        # 出力の確率を降順にソートし、対応するクラスのインデックスも取得
        batch_probs, batch_indices = batch_probs.sort(dim=1, descending=True)

        # クラス名を取得
        class_names = get_classes()

        # 各入力データに対して、上位3つの予測結果をURLのペアとともにJSON化
        for probs, indices in zip(batch_probs, batch_indices):
            for k in range(3):
                results[url].append({"class": class_names[indices[k]], "prob": probs[k].item()})
    # JSONをファイル出力
    file = f'{args.output}/output-{args.id}.json'
    with open(file, 'w') as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    if args.s3_token and args.s3_secret and args.s3_bucket:
        # S3クライアントの作成
        s3 = boto3.client(
            's3',
            endpoint_url=args.s3_endpoint if args.s3_endpoint else None,
            aws_access_key_id=args.s3_token,
            aws_secret_access_key=args.s3_secret,
        )
        # ファイルアップロード
        s3.upload_file(
            Filename=file,
            Bucket=args.s3_bucket,
            Key=os.path.basename(file),
        )

def get_classes():
    # クラス名のリストを保持するファイルが存在するか確認
    if not Path("data/imagenet_class_index.json").exists():
        # ファイルが存在しない場合、ダウンロードして保存
        download_url("https://git.io/JebAs", "data", "imagenet_class_index.json")

    # クラス一覧をJSONファイルから読み込む
    with open("data/imagenet_class_index.json") as f:
        data = json.load(f)
        class_names = [x["ja"] for x in data]    # 日本語のクラス名をリスト化
    return class_names

if __name__ == "__main__":
    main()    # メイン関数を実行
```

### Dockerイメージのビルド

Dockerイメージをビルドします。この時、コンテナレジストリのドメイン名を使ってイメージ名を作成してください。

`EXAMPLE.sakuracr.jp` の場合、 `EXAMPLE.sakuracr.jp/` といった名前になります。 `torchvision` は適当に分かりやすいものを付けます。

```bash
$ sudo docker build -t EXAMPLE.sakuracr.jp/torchvision .
```

実際の動作を確認する際には、`docker run` でコンテナーを起動して動作確認をしてください。出力ファイルを確認する際には、適当なディレクトリを `/opt/artifact` にマウントしてください。

```bash
$ sudo docker run -e URL=https://live.staticflickr.com/5482/11061605383_a847bf1783_b.jpg \
    -e SAKURA_TASK_ID=001 \
    -e SAKURA_ARTIFACT_DIR=/opt/artifact \
    -v /path/to/data/:/opt/artifact \
    -t EXAMPLE.sakuracr.jp/torchvision
```

これで、`/path/to/data/` に `output-001.json` というファイルが作成されているはずです。

## 4. DOKにイメージをアップロード

作成したDockerイメージをDOKにアップロードします。まずコンテナレジストリにログインします。コンテナレジストリのドメイン、ユーザー名、パスワードを入力してください。

```bash
$ sudo docker login EXAMPLE.sakuracr.jp
Username:
Password:
```

ログインしたら、イメージをプッシュします。

```bash
$ sudo docker image push EXAMPLE.sakuracr.jp/torchvision
```

これでプッシュが終われば、準備は完了です。

## 5. DOKでコンテナーを起動・実行

### レジストリーの登録

DOKのダッシュボードで、 `レジストリー` を選択して、 `新規登録` を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/17a5ae43-a01f-f840-b3dc-6d4e821b85c8.png)


以下のように入力してください。入力したら、 `登録` を押します。

| 項目                 | 設定                             |
| -------------------- | -------------------------------- |
| ホスト名             | 例）EXAMPLE.sakuracr.jp          |
| ユーザー名           | コンテナレジストリで登録したもの |
| パスワード           | コンテナレジストリで登録したもの |
| パスワード（確認用） | コンテナレジストリで登録したもの |

### タスクの作成

DOKのダッシュボードで、 `タスク` を選択して、 `新規作成` を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8700cda0-552b-4572-8961-295c6f31f939.png)

以下のように入力してください。入力したら、 `作成` を押します。 `:latest` をつけて、レジストリの最新イメージを指定しています。

| 項目               | 設定                                   |
| ------------------ | -------------------------------------- |
| イメージ           | EXAMPLE.sakuracr.jp/torchvision:latest |
| レジストリー       | 登録したレジストリー                   |
| コマンド           | 何もなし                               |
| エントリーポイント | 何もなし                               |
| 環境変数           | 後述                                   |
| プラン             | v100-32gbまたはh100-80gb               |

環境変数は、以下のように設定してください。

| 項目               | 設定                         |
| ------------------ | ---------------------------- |
| URL              | 画像のURL。カンマ区切りで複数指定できます |

![FireShot Capture 049 - 高火力 DOK - タスク › 新規作成 - secure.sakura.ad.jp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/495ffc3e-e8ba-d9af-6ac6-fe9e50f11a98.jpeg)

タスクを作成するとキューに入り、順番に処理されます。

### 結果の確認

結果はタスクの画面で確認できます。完了していれば、 `アーティファクト` にて、出力ファイルをダウンロードできます。なお、ファイルはtar.gz形式で圧縮されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/5d5ace4b-0427-f928-fb6a-072088120a73.png)

たとえば以下のような内容です。

```json
{
  "https://live.staticflickr.com/5482/11061605383_a847bf1783_b.jpg": [
    {
      "class": "火山",
      "prob": 0.8842170834541321
    },
    {
      "class": "湖畔",
      "prob": 0.10742084681987762
    },
    {
      "class": "谷",
      "prob": 0.0028367205522954464
    }
  ],
  "https://live.staticflickr.com/2106/2207159142_8206ab6984.jpg": [
    {
      "class": "タビー",
      "prob": 0.6061648726463318
    },
    {
      "class": "虎猫",
      "prob": 0.23757117986679077
    },
    {
      "class": "エジプトの猫",
      "prob": 0.10088401287794113
    }
  ],
  "https://live.staticflickr.com/1639/25622749924_3c735ed437_b.jpg": [
    {
      "class": "虎",
      "prob": 0.704362690448761
    },
    {
      "class": "虎猫",
      "prob": 0.2918696403503418
    },
    {
      "class": "ヒョウ",
      "prob": 0.0005688812234438956
    }
  ]
}
```

今回の写真と並べると、以下のような結果でした。

![スクリーンショット 2024-10-11 17.26.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/69d7acba-dd1b-6be6-e1fe-f186d657eb25.png)

写真は以下よりお借りしました。

- [tiger | Openverse](https://openverse.org/image/1271fa19-cd13-46cc-ab77-26f8a984d86b?q=tiger)
- [Cat | Openverse](https://openverse.org/image/a6dd2414-7695-4f03-9723-a9dc6852e2b3?q=cat)
- [Momiji and Mount Fuji | Openverse](https://openverse.org/image/076f5950-cd58-46b1-836a-b15100feb126?q=fuji)

## おまけ（オブジェクトストレージを使う）

アーティファクトの保存期限は72時間なので、データを永続的に残しておきたい場合はオブジェクトストレージへの保存をお勧めします。

### オブジェクトストレージの作成

さくらのクラウドのダッシュボードで、 `オブジェクトストレージ` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/31c2bd38-b083-c446-7be5-7765e8601930.png)


バケットとアクセスキーを作成します。シークレットアクセスキーも生成されるので、保存しておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f368a79e-af79-ed87-e6ab-5b29ebfff193.png)


情報としては、以下が手に入ります（例）。

- S3エンドポイント  
s3.isk01.sakurastorage.jp
- バケット名  
自分で決めたもの
- アクセスキー  
自動生成されたもの
- シークレットアクセスキー  
自動生成されたもの

そして、これらの情報をDOKのタスク実行時の環境変数に設定してください。

| 項目        | 設定                     |
| ----------- | ------------------------ |
| S3_ENDPOINT | S3エンドポイント         |
| S3_BUCKET   | バケット名               |
| S3_TOKEN    | アクセスキー             |
| S3_SECRET   | シークレットアクセスキー |

これでDOKのタスクを実行すると、結果ファイルがオブジェクトストレージにアップロードされます。ファイルアップロードの場合は、圧縮されていない状態で取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8242b162-1f3d-e2d5-2b59-f6cf22edfd2e.png)

## まとめ

今回は、DOKでtorchvision（学習済み画像分類モデル）を試してみました。Dockerイメージを作成する必要があるので、Dockerの知識とDockerイメージを作成できる環境を用意する必要があるので注意してください。

pytorchでは他のモデルを使うメソッドも用意されているので、精度の高いものも試してみてください。

[Pytorch – 学習済みモデルで画像分類を行う方法 | pystyle](https://pystyle.info/pytorch-how-to-use-pretrained-model/)

Dockerイメージさえできてしまえば、後は環境変数を指定して実行すれば良いだけなので簡単です。ぜひAI・機械学習などに活用してください。

[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） | さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)