機械学習のためのDockerと、AWS SageMakerを最低限使うための本を書いた

機械学習のためのDocker/AWS 入門/SageMakerの統一的な本を書いたので紹介していきたいと思います。

![frontpage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/9f6c090f-efd2-6c97-f9fa-6eb54192dd70.jpeg)


## Dockerとは？機械学習との関わりとは？
Dockerとは、「コンテナ仮想化を用いてアプリケーションを開発・配置・実行するためのオープンプラットフォーム」（Wikipediaより）です。

![docker_logo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/5f48d607-7beb-3f8a-4fb8-e093959f2823.jpeg)

Dockerの基本的な説明は多数解説記事があるので割愛しますが、Dockerの大きな特徴として「使い捨てできる環境」が紹介されることがあります。機械学習での使い捨て環境といえば「Google Colaboratory」があり、「Colabで十分だからDockerいらないのでは」と思っている方も一定数いるでしょう。

Colabは非常に有用で、「とりあえずテスト感覚でコードを動かしてみる使い捨て環境」としては便利です。「使い捨てではない」本番を見据えた開発や、「ハイパーパラメーターやビッグデータの分散処理」など実践的なデータ分析ではColabでは不十分になってくることも多いです。このとき、機械学習でもDockerの有用性が出てきます。

## 機械学習でDockerを使うと何が嬉しいのか
### CUDAのバージョンを切り替えができる
PyTorchやTensorFlowといったディープラーニングのフレームワークでGPUを使う場合、CUDAのバージョンに依存します。

![fig01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/aef654c9-e309-b45e-095d-ab5340567a0f.png)

「TensorFlowのバージョンをあげようとしたら、CUDAのバージョンが対応してなくて、CUDAのインストールからやらないといけないのが面倒」という経験をした方もいるのではないでしょうか。

最新バージョンにアップデートするだけならこれでも良いのですが、例えば「ある論文Aのコードを使うために、一時的にフレームワークのバージョンを下げないといけない場合」は、使い終わったあと、また新しいバージョンの再インストールをしないといけなく、正直やってられませんよね。

Dockerならベースのイメージを変えれば**CUDAのバージョンを自在に切り替えられ**ます。例えば、CUDA10.2+Ubuntu18.04なら、

```dockerfile
FROM nvidia/cuda:10.2-cudnn8-devel-ubuntu18.04
```

CUDA11.3+Ubuntu20.04なら、ベースイメージのFROM～を変更するだけでOKです。


```dockerfile
FROM nvidia/cuda:11.2.0-cudnn8-devel-ubuntu20.04
```

自前でDockerをビルドしなくても、NVIDIA-dockerのPyTorchのイメージを使い、最新のPyTorch環境を簡単に利用できます。

```console
docker pull nvcr.io/nvidia/pytorch:22.07-py3
```

**CUDAの環境を自在に変更できる**、これがDockerの大きなメリットといえるでしょう。

### ColabがDocker使えなくサイレントでライブラリのバージョンが変わる
これはDockerのメリットというより、Colabのデメリットですが、**Colabではインストールされているライブラリのバージョンが予告なく変更され、アップデート履歴をトレースできません**。

Dockerイメージのビルドで使うDockerfileでは、ライブラリのバージョンを指定できます。

```dockerfile
# Sklearnの0.2.2を使いたい場合
RUN pip --no-cache-dir install sklearn==0.2.2
```

バージョンに由来するバグは稀によくあるので、バージョンの再現性を保証しやすいというのもメリットでしょう。

### Windowsの場合WSL上で実行すると高速になることがある
特にこれはWindows上でPyTorchを動かすケースですが、Windows上で訓練を動かすよりも、Windows11付属のWSLにDocker（Ubuntu）を立ち上げ、Docker上で訓練したほうが訓練が速いことが確認できました。これは私が検証した例ですが、CIFAR-10をResNet-50で訓練したとき、WSL上のほうが、倍ぐらいの速度が出ているのが確認できます（単位：病）。

**Windows＋コマンドラインの場合**

|       手法       |  1回目 |  2回目 |  3回目 |  平均  |
|:----------------:|:------:|:------:|:------:|:------:|
|      PyTorch     |  920.5 |  921.7 |   912  |  918.1 |
|     Lightning    | 1077.8 | 1081.3 | 1073.9 | 1077.7 |
| Lightning(Mixed) | 1014.9 | 1012.7 | 1013.2 | 1013.6 |

**WSL2＋NVIDIA-docker**

|       手法       | 1回目 | 2回目 | 3回目 |  平均 |
|:----------------:|:-----:|:-----:|:-----:|:-----:|
|      PyTorch     | 463.3 | 471.8 | 466.8 | 467.3 |
|     Lightning    | 498.6 | 503.2 | 522.1 |  508  |
| Lightning(Mixed) | 446.6 | 430.9 | 446.1 | 441.2 |

詳細：[Windows11でWSL2＋nvidia-dockerでPyTorchを動かすのがすごすぎた](https://blog.shikoan.com/wsl2-ndivid-docker-pytorch/)

おそらくLinux上のほうがCUDAの最適化がかかりやすいのでしょうが、WindowsでもWSLを使って、**Docker内で動かしたほうが単純に速い**というメリットがあります。

## Dockerを使った機械学習の例

例えば、次のようなオートエンコーダの例を考えましょう。

```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torchvision
import pytorch_lightning as pl
import argparse
from PIL import Image
import numpy as np
import os

class AutoEncoder(pl.LightningModule):
    def __init__(self, opt):
        super().__init__()
        self.opt = opt

        self.conv1 = nn.Conv2d(1, 16, 3, stride=2, padding=1)
        self.conv1_bn = nn.BatchNorm2d(16)
        self.conv2 = nn.Conv2d(16, 32, 3, stride=2, padding=1)
        self.conv2_bn = nn.BatchNorm2d(32)
        self.deconv1 = nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1)
        self.deconv1_bn = nn.BatchNorm2d(16)
        self.deconv2 = nn.ConvTranspose2d(16, 1, 3, stride=2, padding=1, output_padding=1)

        self.cache_validation_batch = None

    def forward(self, inputs):
        x = F.relu(self.conv1_bn(self.conv1(inputs)))
        x = F.relu(self.conv2_bn(self.conv2(x)))
        x = F.relu(self.deconv1_bn(self.deconv1(x)))
        x = torch.sigmoid(self.deconv2(x))
        return x

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
        return optimizer

    def training_step(self, train_batch, batch_idx):
        x, _ = train_batch
        x_pred = self.forward(x)
        loss = F.l1_loss(x_pred, x)
        self.log("train_loss", loss, prog_bar=False, logger=True)
        return loss

    def write_outputs(self, batch):
        grid = torchvision.utils.make_grid(
            batch[:64], nrow=8, value_range=(0.0, 1.0)
        ) # (C, H, W)
        grid = grid.cpu().numpy().transpose([1, 2, 0]) # (H, W, C)
        grid = (grid * 255.0).astype(np.uint8)

        os.makedirs(self.opt.output_dir, exist_ok=True)
        with Image.fromarray(grid) as img:
            img.save(f"{self.opt.output_dir}/epoch_{self.current_epoch:03}.png")

    def validation_step(self, val_batch, batch_idx):
        x, _ = val_batch
        x_pred = self.forward(x)
        if batch_idx == 0:
            # ここで直接吐くとDocker環境で「AssertionError: can only test a child process」という警告文が出る（処理は続く）のでキャッシュさせる
            # ローカル環境でエラーは出ないので、Docker→ローカルのI/Oボトルネックと、マルチプロセスが噛み合っていないのかも？
            #self.write_outputs(x_pred) 
            self.cache_validation_batch = x_pred
        loss = F.l1_loss(x_pred, x)
        self.log("val_loss", loss, prog_bar=True, logger=True)

    def validation_epoch_end(self, outputs):
        if self.cache_validation_batch is not None:
            self.write_outputs(self.cache_validation_batch)
            self.cache_validation_batch = None

class KMNISTModule(pl.LightningDataModule):
    def __init__(self, opt):
        super().__init__()
        self.opt = opt

    def prepare_data(self):
        self.train_dataset = torchvision.datasets.KMNIST(
            self.opt.data_dir, train=True, download=True,
            transform=torchvision.transforms.ToTensor())
        self.val_dataset = torchvision.datasets.KMNIST(
            self.opt.data_dir, train=False, download=True,
            transform=torchvision.transforms.ToTensor())

    def train_dataloader(self):
        return DataLoader(self.train_dataset, batch_size=256, num_workers=4, shuffle=True)

    def val_dataloader(self):
        return DataLoader(self.val_dataset, batch_size=256, num_workers=4, shuffle=False)

def main(opt):
    model = AutoEncoder(opt)
    cifar = KMNISTModule(opt)

    if opt.gpus == 0:
        train_flag = {"accelerator":"cpu"}
    elif opt.gpus > 0:
        train_flag = {"accelerator":"gpu", "devices":opt.gpus}

    trainer = pl.Trainer(max_epochs=10, **train_flag)
    trainer.fit(model, cifar)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="MNIST Training")
    parser.add_argument("--data_dir", type=str, default="./data")
    parser.add_argument("--ckpt_dir", type=str, default="./ckpt")
    parser.add_argument("--output_dir", type=str, default="./output")
    parser.add_argument("--gpus", type=int, default=0) # 0-> CPU, 1 -> use 1 gpu

    opt = parser.parse_args()

    main(opt)
```

これをDockerのボリュームマウントを使って動かすと

```console
docker run -it --rm --gpus all -v $(pwd):/mnist some_images
```

```console
root@118805f6912b:/# cd mnist
root@118805f6912b:/mnist# python kmnist_autoencoder.py
```

カレントディレクトリがDocker内の「/mnist」としてマウントされます。つまり、**Docker内で訓練プログラムを動かしつつ、ログや途中生成結果、訓練済みモデルをWindows上へリアルタイムで吐き出すことが可能**です。

## SageMaker 
本書の最後では、AWSのサービスの1つであるSageMaker、特にSageMaker Training Job（便利ではあるが使い方に癖のある）を独自にビルドしたコンテナを使って、自在に扱うまでの手順を紹介しています。本書の最後では、Training Jobを使って、YouTube上の動画をダウンロードしYOLOXを推論し、Bounding Boxをレンダリングした後の動画をOpenH264でエンコードする方法を例示しています。

![14_15_yolox_video.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/877dcfd8-fb30-2d25-580b-84176542deb5.png)

元動画：[https://www.youtube.com/watch?v=-SQhoG6PsKg](https://www.youtube.com/watch?v=-SQhoG6PsKg)

SageMakerに興味がある、使っている方には楽しめる内容になっていると思います。

## 目次
本書の目次は以下の通りです。

### 第1章：Docker入門
* なぜDockerなのか？
* CUDAのバージョン問題
* 論文の公式実装の環境依存問題
* Colabが本番運用に向かない問題
* WSL2導入
* WSL2上でのDocker-CEのインストール
* Dockerイメージとコンテナの概念の理解
* Dockerは複数の環境を切り替えられる
* インスタンスとしてのコンテナ
* 演習問題（１）
* Dockerイメージのビルド（docker build）
* WSL2におけるWindowsのファイルの扱い
* Dockerイメージを削除する（docker rmi）
* 事例：Pythonのイメージを作る
* コンテナへのファイルコピー（docker cp）
* 独自Dockerfileの作り方・考え方
* docker runのボリュームのマウントによるお手軽デバッグ
* 演習問題（２）

### 第2章：AWS S3/ECR 
* AWSアカウントの登録
* MFAの設定
* 独自のアカウント名をつける
* IAMユーザー
* Amazon S3
* ブラウザ上からのS3操作
* AWS CLIインストール
* aws configure
* AWS CLIによるS3のオブジェクト操作
* アップロード・ダウンロード（s3 cp）
* ファイル移動や名前変更（s3 mv）
* ファイルの削除（s3 rm）
* ディレクトリ同期でコピー削除を同時にこなす（s3 sync）
* 演習問題（1）
* Boto3の導入と認証
* Boto3によるS3オブジェクトの操作
* バケットのバージョニング
* バージョニング環境下でのファイル操作
* バケットのライフサイクルルール
* Amazon ECR
* 演習問題（2）

### 第3章：SageMaker Notebook 
* SageMaker ノートブックとは
* SageMakerノートブックとColabの違い
* SageMakerノートブックの料金計算
* ノートブックインスタンスの初期化
* ノートブックインスタンスの動作確認
* ノートブックインスタンスとS3の連携
* ノートブックインスタンスとECRの連携
* まとめ

### 第4章：SageMaker Training Job
* SageMaker Training job
* ケース1：MNIST
* ケース2：シェルファイルの実行
* ケース3：独自DockerイメージでTraining Jobを動作させる
* ケース4：実行ファイルが複数ある場合
* ケース5：ハイパーパラメータと環境変数
* ケース6：Training Jobの並列実行
* ケース7：訓練データをS3から入力する
* ケース8：スポットインスタンスによる訓練
* ケース9：Training Jobのローカルモード
* ケース10：Training Jobを使用したOpenH264による動画エンコード
* ケース11：Stable Diffusion
* 演習問題

### 模範解答

## 試し読み・通販
試し読みはこちらから
[https://koshian2.booth.pm/items/4150087](https://koshian2.booth.pm/items/4150087)

通販はこちらからどうぞ
* **[https://koshian2.booth.pm/items/4150087](https://koshian2.booth.pm/items/4150087)**
* **[https://techbookfest.org/product/aMV8vVfvdXQcKEFkYbkjEL](https://techbookfest.org/product/aMV8vVfvdXQcKEFkYbkjEL)**