機械学習初学者が Amazon SageMaker で Stable Diffusion を動かしてみた

こちらは [AWS for Games Advent Calendar 2022](https://qiita.com/advent-calendar/2022/aws-for-games) の12月7日分のエントリーです。

## はじめに

機械学習の入門ということで、Stable DiffusionをAmazon SageMaker上で動かし、AIイラストを出力してみようと思います。

Amazon SageMakerには多くの機能があり初学者はどこから初めてよいか分かりづらいと思いますが、この記事がその取っ掛かりとなれば幸いです。


## 目標

* Amazon SageMaker上でStable Diffusionを動かし、AIイラストを作成する
* 機械学習の流れを理解する



## TL;DR

* 機械学習とは
* Amazon SageMakerとは
* Hugging Faceアカウント作成
    * Stable Diffusion ライセンス同意
    * アクセストークン作成
* ノートブックインスタンス作成
    * ノートブックインスタンス設定
    * アクセス許可と暗号化
    * Jupyter Notebookログイン
* 推論モデル作成
* SageMakerエンドポイント作成
* 画像生成
* お片付け
* 終わりに



## 機械学習とは

そもそも機械学習とは何なのかですが、簡単に言うと膨大なデータをコンピュータに登録することでそのデータからルールやパターンを発見させ、その結果を基にさまざまな分析や予測に利用するというものです。
我々の身近なところで機械学習が利用されているケースとしては、ECサイトのおすすめ商品です。
これはユーザーの購入履歴から他の似たようなユーザーの購入情報を利用して次に購入しそうな商品を提示しています。
今回扱うStable Diffusionでも膨大な画像データを学習しており、「猫」というテキスト情報からこれまでに学習した画像データを基にして新たに猫の画像を生成します。


## Amazon SageMakerとは

そんな機械学習をモデル作成から動作まで一貫して管理してくれるサービスです。
機械学習モデルの作成には多くの計算が必要なので、強力なCPU・GPU等のマシンリソースやそれを動かすためのライブラリといった準備が必要不可欠なのですが、それらもAmazon SageMakerが用意してくれているのでお金を払うだけでいつでも利用できます。
今回は既成モデルを利用するので、モデル作成の費用は不要です。
ただし、コマンド実行用のローエンドEC2と画像生成用のGPUを積んだミドルレンジEC2を借りる費用だけは必要です。


## Hugging Faceアカウント作成

Hugging FaceのWebページ( https://huggingface.co/ )にアクセスし、右上部の「Sing Up」からアカウント登録を行います。

### Stable Diffusion ライセンス同意

Stable DiffusionのWebページ(  https://huggingface.co/CompVis/stable-diffusion-v1-4 )のライセンスに同意しておきます。
同意しておかなければ後々にモデルをダウンロードする際に403エラーになります。(1敗)


### アクセストークン作成

アクセストークンを作成します。
画面右上部のアカウントメニューからSettingsを開きます。
![スクリーンショット 2022-11-24 135656.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/234737e9-2f80-32ac-ebb7-f29309724b23.png)
左サイドメニューから「Access Tokens」を選ぶと、トークンを作成する画面があります。
![スクリーンショット 2022-11-24 140018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/e0c08c62-a566-bb65-c2b0-951db834bb81.png)
「New token」をクリックしてわかりやすい名前とread Roleを設定して、作成したアクセストークンをメモしておきます。
![スクリーンショット 2022-11-24 140039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/4a0fe5c5-fe3d-8748-9547-8962d6af3a2b.png)
モデルダウンロード時に必要になります。(1敗)


## ノートブックインスタンス作成

ここからAWS上で作業していきます。
WebコンソールのAmazon SageMakerを開き、ノートブックインスタンスからノートブックインスタンスの新規作成を行います。
![スクリーンショット 2022-11-24 140417.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/40ee3887-031e-d218-07ce-6da4b717269e.png)
ノートブックインスタンスとはJupyter Notebookがインストール済なEC2インスタンスです。
WebブラウザからPythonのプログラムを実行できるので今後の作業が非常に楽になります。


### ノートブックインスタンス設定

![スクリーンショット 2022-11-24 140558.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/37780198-286a-5994-6819-eb32e2e134bb.png)
Stable Diffusionのモデルをダウンロードして圧縮する必要があるので、追加設定からボリュームサイズのみ30GBに拡張しておきます。(1敗)
その他はデフォルト設定で大丈夫です。

### アクセス許可と暗号化

今回はデフォルトのIAMロールで行います。
![スクリーンショット 2022-11-24 140639.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/718d9ea8-c1e9-2cd7-51ba-6436fe416541.png)
ノートブックインスタンスを作成し、ステータスが「InService」になるまで待ちましょう。

## Jupyter Notebookログイン

ノートブックインスタンス作成が完了したら、Jupyter Notebookにログインしましょう。
アクションからJupyterを開くをクリックします。
![スクリーンショット 2022-11-24 141150.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/2127a646-a3dc-1906-8ac7-1df540c56d60.png)
画面右上のNewからconda_pythorch_p38を選択します。
![スクリーンショット 2022-11-24 141245.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/bc666d33-ccc9-9278-2b8c-dc2b7b343c57.png)
このような画面になれば準備完了です。
![スクリーンショット 2022-11-24 141333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/2b88dc16-4770-d0ff-15a7-b081104d1be9.png)
この後はこのテキストボックスにpythonプログラムを入力し、Shift + Enterで実行していきます。


## model.tar.gz作成

Stable Diffusionのモデルをダウンロードし、SageMakerエンドポイントで使えるようにtar.gzで固めていきます。
上から順番にJupyter Notebook上で実行してください。

``` python
!pip install "sagemaker==2.116.0" "huggingface_hub==0.10.1" --upgrade --quiet
```

``` python
import sagemaker
import boto3
sess = sagemaker.Session()
# sagemaker session bucket -> used for uploading data, models and logs
# sagemaker will automatically create this bucket if it not exists
sagemaker_session_bucket=None
if sagemaker_session_bucket is None and sess is not None:
    # set to default bucket if a bucket name is not given
    sagemaker_session_bucket = sess.default_bucket()

try:
    role = sagemaker.get_execution_role()
except ValueError:
    iam = boto3.client('iam')
    role = iam.get_role(RoleName='sagemaker_execution_role')['Role']['Arn']

sess = sagemaker.Session(default_bucket=sagemaker_session_bucket)

print(f"sagemaker role arn: {role}")
print(f"sagemaker bucket: {sess.default_bucket()}")
print(f"sagemaker session region: {sess.boto_region_name}")
```

``` python
!mkdir code
```

``` python
%%writefile code/requirements.txt
diffusers==0.6.0
transformers==4.23.1
```

``` python
%%writefile code/inference.py
import base64
import torch
from io import BytesIO
from diffusers import StableDiffusionPipeline


def model_fn(model_dir):
    # Load stable diffusion and move it to the GPU
    pipe = StableDiffusionPipeline.from_pretrained(model_dir, torch_dtype=torch.float16)
    pipe = pipe.to("cuda")

    return pipe


def predict_fn(data, pipe):

    # get prompt & parameters
    prompt = data.pop("inputs", data)
    # set valid HP for stable diffusion
    num_inference_steps = data.pop("num_inference_steps", 50)
    guidance_scale = data.pop("guidance_scale", 7.5)
    num_images_per_prompt = data.pop("num_images_per_prompt", 4)

    # run generation with parameters
    generated_images = pipe(
        prompt,
        num_inference_steps=num_inference_steps,
        guidance_scale=guidance_scale,
        num_images_per_prompt=num_images_per_prompt,
    )["images"]

    # create response
    encoded_images = []
    for image in generated_images:
        buffered = BytesIO()
        image.save(buffered, format="JPEG")
        encoded_images.append(base64.b64encode(buffered.getvalue()).decode())

    # create response
    return {"generated_images": encoded_images}
```

``` python
from distutils.dir_util import copy_tree
from pathlib import Path
from huggingface_hub import snapshot_download
import random

HF_MODEL_ID="CompVis/stable-diffusion-v1-4"
HF_TOKEN="" # Hugging Faceのアクセストークンを入力してください
assert len(HF_TOKEN) > 0, "Please set HF_TOKEN to your huggingface token. You can find it here: https://huggingface.co/settings/tokens"

# download snapshot
snapshot_dir = snapshot_download(repo_id=HF_MODEL_ID,revision="fp16",use_auth_token=HF_TOKEN)

# create model dir
model_tar = Path(f"model-{random.getrandbits(16)}")
model_tar.mkdir(exist_ok=True)

# copy snapshot to model dir
copy_tree(snapshot_dir, str(model_tar))
```

``` python
# copy code/ to model dir
copy_tree("code/", str(model_tar.joinpath("code")))
```

``` python
import tarfile
import os

# helper to create the model.tar.gz
def compress(tar_dir=None,output_file="model.tar.gz"):
    parent_dir=os.getcwd()
    os.chdir(tar_dir)
    with tarfile.open(os.path.join(parent_dir, output_file), "w:gz") as tar:
        for item in os.listdir('.'):
          print(item)
          tar.add(item, arcname=item)
    os.chdir(parent_dir)

compress(str(model_tar))
```



## model.tar.gzのS3アップロード

固めたmodel.tar.gzをS3にアップロードします。
今回はSageMakerのデフォルトバケット内に保存します。

``` python
from sagemaker.s3 import S3Uploader

# upload model.tar.gz to s3
s3_model_uri=S3Uploader.upload(local_path="model.tar.gz", desired_s3_uri=f"s3://{sess.default_bucket()}/stable-diffusion-v1-4")

print(f"model uploaded to: {s3_model_uri}")
```


## SageMaker エンドポイント作成

SageMakerエンドポイントを作成します。

``` python
from sagemaker.huggingface.model import HuggingFaceModel


# create Hugging Face Model Class
huggingface_model = HuggingFaceModel(
   model_data=s3_model_uri,      # path to your model and script
   role=role,                    # iam role with permissions to create an Endpoint
   transformers_version="4.17",  # transformers version used
   pytorch_version="1.10",       # pytorch version used
   py_version='py38',            # python version used
)

# deploy the endpoint endpoint
predictor = huggingface_model.deploy(
    initial_instance_count=1,
    instance_type="ml.g4dn.xlarge"
)
```

## 画像生成

実際にSageMakerエンドポイントにアクセスしてイラストを出力してみましょう。

``` python
from PIL import Image
from io import BytesIO
from IPython.display import display
import base64
import matplotlib.pyplot as plt

# helper decoder
def decode_base64_image(image_string):
  base64_image = base64.b64decode(image_string)
  buffer = BytesIO(base64_image)
  return Image.open(buffer)

# display PIL images as grid
def display_images(images=None,columns=3, width=100, height=100):
    plt.figure(figsize=(width, height))
    for i, image in enumerate(images):
        plt.subplot(int(len(images) / columns + 1), columns, i + 1)
        plt.axis('off')
        plt.imshow(image)
```

``` python
response = predictor.predict(data={
  "inputs": "1 girl",
  "num_images_per_prompt" : 1,
  "num_inference_steps" : 25,
  "guidance_scale" : 7.5
  }
)

# decode images
decoded_images = [decode_base64_image(image) for image in response["generated_images"]]

# visualize generation
display_images(decoded_images)
```

暫く待つとAIイラストが表示されます。
inputs部分を書き換えれば自由に好きなイラストを作成できます。
![test_2022-11-21-00-46-23.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/4a00a013-f4e8-e281-9b73-1abb5eac9adf.jpeg)


## お片付け

SageMakerのエンドポイントを削除します。

``` python
predictor.delete_model()
predictor.delete_endpoint()
```

ノートブックインスタンスを削除します。
インスタンスを停止してから削除します。
![スクリーンショット 2022-11-24 145406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/e231ac85-1d3d-5c51-a2b8-fa59342c4817.png)
![スクリーンショット 2022-11-24 145730.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/71903fa9-c32f-8cd7-ea40-312b8fe01eb4.png)

S3上のmodel.tar.gzを削除します。
![スクリーンショット 2022-11-24 150043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138311/a843f763-8b78-98e2-340d-74b7bcca545b.png)



以上でお片付け完了です。
お疲れ様でした。


## 終わりに

Amazon SageMakerを利用したStable Diffusionの利用方法についてご紹介しました。
Stable Diffusion以外にもHugging Faceには様々なモデルがあるので是非動かしてみてください。

それでは、残りの年末も良い機械学習ライフをお過ごし下さい。


(免責) 本記事の内容はあくまでも個人の意見であり、所属する企業や団体とは関係がございません。