DOKはコンテナー型のGPUサービスで、NVIDIA V100とかH100を実行時間課金で利用できるサービスです。
コンテナー型GPUクラウドサービス 高火力 DOK(ドック) | さくらインターネット
今回はこのDOKで、torchvision(学習済みモデルを使った画像分類)を試してみました。
実行までのステップ
- さくらのクラウドのアカウントの用意(必須)
- Dockerイメージのビルド環境(Linuxなど)を用意
- Dockerイメージのビルド
- DOKにイメージをアップロード
- DOKでコンテナーを起動・実行
1. さくらのクラウドのアカウントの用意
DOKはさくらのクラウドに紐付いたサービスなので、さくらのクラウドのアカウント必須です。
コンテナレジストリの作成
さくらのクラウドにログインしたら さくらのクラウド
を選択します。
左側のメニューの LAB
の中にある コンテナレジストリ
を選択します。
追加
を押して、コンテナレジストリを作成します。最低限、以下の入力が必要です。
項目 | 設定 |
---|---|
名前 | 分かりやすい、任意の名前を入力してください |
コンテナレジストリ名 | ドメイン名に使われます。 EXAMPLE.sakuracr.jp というコンテナレジストリになります |
公開設定 | 非公開で良いかと思います |
ユーザーの作成
コンテナレジストリにアクセスできるユーザーを作成します。作成したコンテナレジストリをダブルクリックして、詳細を表示します。
ユーザー
タブを選択して、追加
を押します。
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イメージのビルド
作業フォルダを作成します。
mkdir torchvision
cd torchvision
後はこの torchvision
ディレクトリの中で処理を書いていきます。
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コンテナー起動時に実行するスクリプトです。内容は以下の通りです。
#!/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を作成します。
requests
torch
torchvision
boto3
runner.pyの作成
runner.pyは実際に実行するスクリプトです。内容は以下の通りです。
ライブラリの読み込み
最初にライブラリを読み込みます。
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()
GPU判定用関数の作成
GPUが利用するかどうか判定する関数 get_device
を作成します。
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
を作成します。
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
関数に実装します。
def main():
# この中に実装します
if __name__ == "__main__":
main() # メイン関数を実行
モデルの読み込み
必要なモデルをダウンロードして読み込みます。
# デバイス(GPUまたはCPU)を取得する
device = get_device(use_gpu=True)
# ResNet50モデルを読み込み、指定したデバイスに転送する
model = torchvision.models.resnet50(pretrained=True).to(device)
画像を前処理するための変換処理を定義する
画像を前処理するための変換処理を transforms.Compose
で定義します。
# 画像を前処理するための変換処理を定義
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
を ,
で分割して、その分だけ処理を行います。
# 環境変数URLを取得、,で分割
urls = args.url.split(',')
results = {}
# URLの数だけループ
for url in urls:
# 以下はこの中
画像のダウンロード
画像を取得して、 RGB
に変換します。
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)
クラス名を取得する
クラス名を取得します。クラス名は上位3つの予測結果を利用します。
# クラス名を取得
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
ディレクトリに保存します。
# 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を使って出力内容をアップロードします。
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
の全体のコードは以下の通りです。
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
は適当に分かりやすいものを付けます。
$ sudo docker build -t EXAMPLE.sakuracr.jp/torchvision .
実際の動作を確認する際には、docker run
でコンテナーを起動して動作確認をしてください。出力ファイルを確認する際には、適当なディレクトリを /opt/artifact
にマウントしてください。
$ 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にアップロードします。まずコンテナレジストリにログインします。コンテナレジストリのドメイン、ユーザー名、パスワードを入力してください。
$ sudo docker login EXAMPLE.sakuracr.jp
Username:
Password:
ログインしたら、イメージをプッシュします。
$ sudo docker image push EXAMPLE.sakuracr.jp/torchvision
これでプッシュが終われば、準備は完了です。
5. DOKでコンテナーを起動・実行
レジストリーの登録
DOKのダッシュボードで、 レジストリー
を選択して、 新規登録
を押します。
以下のように入力してください。入力したら、 登録
を押します。
項目 | 設定 |
---|---|
ホスト名 | 例)EXAMPLE.sakuracr.jp |
ユーザー名 | コンテナレジストリで登録したもの |
パスワード | コンテナレジストリで登録したもの |
パスワード(確認用) | コンテナレジストリで登録したもの |
タスクの作成
DOKのダッシュボードで、 タスク
を選択して、 新規作成
を押します。
以下のように入力してください。入力したら、 作成
を押します。 :latest
をつけて、レジストリの最新イメージを指定しています。
項目 | 設定 |
---|---|
イメージ | EXAMPLE.sakuracr.jp/torchvision:latest |
レジストリー | 登録したレジストリー |
コマンド | 何もなし |
エントリーポイント | 何もなし |
環境変数 | 後述 |
プラン | v100-32gbまたはh100-80gb |
環境変数は、以下のように設定してください。
項目 | 設定 |
---|---|
URL | 画像のURL。カンマ区切りで複数指定できます |
タスクを作成するとキューに入り、順番に処理されます。
結果の確認
結果はタスクの画面で確認できます。完了していれば、 アーティファクト
にて、出力ファイルをダウンロードできます。なお、ファイルはtar.gz形式で圧縮されています。
たとえば以下のような内容です。
{
"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
}
]
}
今回の写真と並べると、以下のような結果でした。
写真は以下よりお借りしました。
おまけ(オブジェクトストレージを使う)
アーティファクトの保存期限は72時間なので、データを永続的に残しておきたい場合はオブジェクトストレージへの保存をお勧めします。
オブジェクトストレージの作成
さくらのクラウドのダッシュボードで、 オブジェクトストレージ
を選択します。
バケットとアクセスキーを作成します。シークレットアクセスキーも生成されるので、保存しておいてください。
情報としては、以下が手に入ります(例)。
- S3エンドポイント
s3.isk01.sakurastorage.jp - バケット名
自分で決めたもの - アクセスキー
自動生成されたもの - シークレットアクセスキー
自動生成されたもの
そして、これらの情報をDOKのタスク実行時の環境変数に設定してください。
項目 | 設定 |
---|---|
S3_ENDPOINT | S3エンドポイント |
S3_BUCKET | バケット名 |
S3_TOKEN | アクセスキー |
S3_SECRET | シークレットアクセスキー |
これでDOKのタスクを実行すると、結果ファイルがオブジェクトストレージにアップロードされます。ファイルアップロードの場合は、圧縮されていない状態で取得できます。
まとめ
今回は、DOKでtorchvision(学習済み画像分類モデル)を試してみました。Dockerイメージを作成する必要があるので、Dockerの知識とDockerイメージを作成できる環境を用意する必要があるので注意してください。
pytorchでは他のモデルを使うメソッドも用意されているので、精度の高いものも試してみてください。
Pytorch – 学習済みモデルで画像分類を行う方法 | pystyle
Dockerイメージさえできてしまえば、後は環境変数を指定して実行すれば良いだけなので簡単です。ぜひAI・機械学習などに活用してください。