HuggingFace ブログ : Stable Diffusion with 🧨 Diffusers

HuggingFace ブログ : Stable Diffusion with 🧨 Diffusers (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 11/13/2022

* 本ページは、HuggingFace Blog の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

クラスキャット 人工知能 研究開発支援サービス

クラスキャット は人工知能・テレワークに関する各種サービスを提供しています。お気軽にご相談ください :

◆ 人工知能とビジネスをテーマに WEB セミナーを定期的に開催しています。スケジュール
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

  • 株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
  • sales-info@classcat.com  ;  Web: www.classcat.com  ;   ClassCatJP

 

 

HuggingFace ブログ : Stable Diffusion with 🧨 Diffusers

Stable Diffusion は CompVis, Stability AILAION の研究者と技術者により作成されたテキスト-to-画像の潜在拡散モデルです。それは LAION-5B データベースのサブセットの 512×512 画像で訓練されています。LAION-5B は現在存在する最大の、フリーでアクセス可能なマルチモーダルなデータセットです。

この記事では、🧨 Diffusers ライブラリ で Stable Diffusion を使用する方法を示し、モデルがどのように動作するかを說明して最後に diffusers が画像生成パイプラインをどのようにカスタマイズを可能にするかを少し掘り下げたいと思います。

 

Stable Diffusion の実行

ライセンス

モデルを使用する前に、重みをダウンロードして使用するためにモデルライセンスを承認する必要があります。

ライセンスは、そのようなパワフルな機械学習システムの潜在的な弊害を軽減するように設計されています。私たちは ユーザがライセンス全体を注意深く読むことを要求します。ここに要約を提供します :

  1. 違法あるいは有害な出力やコンテンツを意図的に生成または共有するためにモデルを使用できません、

  2. 私たちは貴方が生成した出力の権利を主張しません、貴方はそれらを自由に使用できて、そしてライセンスの条件に反するべきではないそれらの使用について責任を負います、そして

  3. 重みを再配布してモデルを商用 and/or サービスとして利用しても良いです。それを行なう場合、ライセンスのものと同じ使用制限を含めて、そして CreativeML OpenRAIL-M のコピーをすべてのユーザと共有しなければならないことに留意してください。

 

使用方法

最初に、以下のコードスニペットを実行するために diffusers==0.4.0 をインストールする必要があります :

pip install diffusers==0.4.0 transformers scipy ftfy

この記事ではモデルバージョン v1-4 を使用しますので、そのカード にアクセスし、ライセンスを読んで同意するのであればチェックボックスをチェックする必要があります。貴方は 🤗 Hugging Face ハブの登録ユーザである必要があり、そしてコードを動作させるにはアクセストークンを使用する必要もあります。アクセストークンの詳細は、ドキュメントのこのセクション を参照してください。アクセスを要求したのであれば、貴方のユーザトークンを次のように確実に渡してください :

YOUR_TOKEN="/your/huggingface/hub/token"

そのワンタイムのセットアップの後、Stable Diffusion 推論を進めることができます。

Stable Diffusion モデルは StableDiffusionPipeline パイプラインを使用して数行だけで推論を実行できます。このパイプラインは、単純な from_pretrained 関数呼び出しで、テキストから画像を生成するために必要なものすべてをセットアップします。

from diffusers import StableDiffusionPipeline

# get your token at https://huggingface.co/settings/tokens
pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", use_auth_token=YOUR_TOKEN)

If a GPU is available, let’s move it to one!

pipe.to("cuda")

Note : GPU メモリにより制限され 10GB 未満の利用可能な GPU RAM しか持たない場合、StableDiffusionPipeline を (上で行なわれたような) デフォルトの float32 精度ではなく float16 精度でロードすることを確実にしてください。fp16 ブランチから重みをロードし、diffusers に重みが float16 精度であることを想定していることを指示することでそれを行なうことができます :

import torch
from diffusers import StableDiffusionPipeline

# get your token at https://huggingface.co/settings/tokens
pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", revision="fp16", torch_dtype=torch.float16, use_auth_token=YOUR_TOKEN)

To run the pipeline, simply define the prompt and call pipe.

prompt = "a photograph of an astronaut riding a horse"

image = pipe(prompt)["sample"][0]

# you can save the image with
# image.save(f"astronaut_rides_horse.png")

The result would look as follows :

前のコードはそれを実行するたびに異なる画像を与えます。

ある時点で黒い画像を得る場合、それはモデル內部に組み込まれたコンテンツフィルターが NSFW の結果を検出した可能性があります。これは当てはまらないはずと確信するのであれば、プロンプトを調整するか異なるシードの使用を試してみてください。実際、モデル予測は特定の結果に対して NSFW が検出されたかの情報を含みます。Let’s see what they look like:

result = pipe(prompt)
print(result)
{
    'sample': [<PIL.Image.Image image mode=RGB size=512x512>],
    'nsfw_content_detected': [False]
}

決定論的出力を望む場合、ランダムシードを蒔 (ま) いて generator をパイプラインに渡すことができます。同じシードで generator を使用すればいつでも同じ画像出力が得られます。

import torch

generator = torch.Generator("cuda").manual_seed(1024)
image = pipe(prompt, guidance_scale=7.5, generator=generator).images[0]

# you can save the image with
# image.save(f"astronaut_rides_horse.png")

The result would look as follows

num_inference_steps 引数を使用して推論ステップ数を変更できます。

一般に、結果はより多くのステップを使用すればより良くなりますが、より多くのステップは生成により時間がかかります。Stable Diffusion は比較的少ないステップ数で非常に上手く動作しますので、デフォルトの 50 の推論ステップ数を使用することを勧めます。より速い結果を望むのであればより小さい数を使用できます。より高質な結果を潜在的に求めるならば、大きな数を使用できます。

より少ないノイズ除去ステップでパイプラインの実行を試しましょう。

import torch

generator = torch.Generator("cuda").manual_seed(1024)
image = pipe(prompt, guidance_scale=7.5, num_inference_steps=15, generator=generator).images[0]

# you can save the image with
# image.save(f"astronaut_rides_horse.png")

構造が同じでも、宇宙服と馬の一般的な形に問題があることに注意してください。これは、15 だけのノイズ除去ステップは生成結果の質を本質的に劣化させていることを示しています。前述のように、50 のノイズ除去ステップは高品質な画像を生成するのに通常は十分です。

num_inference_steps に加えて、すべてのこれまでの例で guidance_scale と呼ばれる、もう一つの関数引数を使用してきました。guidance_scale は、全体的なサンプル品質に加えて生成をガイドする、条件信号 (この場合はテキスト) への忠実度 (= adherence) を増す方法です。それはまた 分類器フリー・ガイダンス としても知られています、これは簡単に言えば (画像品質と多様性を犠牲にして) 生成がプロンプトに本質的により良く一致するように強制するものです。Stable Diffusion に対しては 7 と 8.5 の間の値が通常は良い選択です。デフォルトではパイプラインは 7.5 の guidance_scale を使用します。

非常に大きい値を使用すれば画像は良く見えるかもしれませんが、多様性は小さくなります。このパラメータの技術的な詳細については記事の このセクション で学習できます。

次に、同じプロンプトの幾つかの画像を一度に生成できる方法を見ましょう。まず、それらをグリッドで素敵に可視化するのに役立つ image_grid 関数を作成します。

from PIL import Image

def image_grid(imgs, rows, cols):
    assert len(imgs) == rows*cols

    w, h = imgs[0].size
    grid = Image.new('RGB', size=(cols*w, rows*h))
    grid_w, grid_h = grid.size
    
    for i, img in enumerate(imgs):
        grid.paste(img, box=(i%cols*w, i//cols*h))
    return grid

同じプロンプトを複数回繰り返したリストを単純に使用して、同じプロンプトに対する複数の画像を生成できます。前に使用した文字列の代わりにリストをパイプラインに送ります。

num_images = 3
prompt = ["a photograph of an astronaut riding a horse"] * num_images

images = pipe(prompt).images

grid = image_grid(images, rows=1, cols=3)

# you can save the grid with
# grid.save(f"astronaut_rides_horse.png")

デフォルトでは、stable diffusion は 512 x 512 ピクセルの画像を生成します。ポートレートかランドスケープの比率で矩形の画像を作成するために height と width 引数を使用してデフォルトをオーバーライドすることは非常に簡単です。

画像サイズを選択するとき、以下のアドバイスを与えます :

  • height と width は両方とも 8 の倍数であることを確実にしてください。

  • 512 以下にすると低い質の画像という結果になるかもしれません。

  • 両方の方向で 512 を超えると画像領域を繰り返します (大域的な一貫性は失われます)。

  • 非正方形画像を作成するベストな方法は、1 つの次元で 512、そして他方でそれより大きい値を使用することです。

Let’s run an example:

prompt = "a photograph of an astronaut riding a horse"
image = pipe(prompt, height=512, width=768).images[0]

# you can save the image with
# image.save(f"astronaut_rides_horse.png")

 

Stable Diffusion はどのように動作するか?

stable diffusion が生成できる高品質な画像を見てきましたので、モデルがどのように機能するかをより良く少し理解してみましょう。

Stable Diffusion は High-Resolution Image Synthesis with Latent Diffusion Models (潜在拡散モデルによる高解像度画像合成) で提案された、潜在拡散 (Latent Diffusion) と呼ばれる特定のタイプの拡散モデルに基づいています。

一般的に言えば、拡散モデルは画像のような関心のあるサンプルに到達するためにランダムなガウスノイズを段階的に除去するように訓練された機械学習システムです。それらがどのように動作するかの詳細な概要については、この colab を確認してください。

拡散モデルは画像データの生成について最先端の結果を獲得することを示してきました。しかし拡散モデルの一つの欠点は、逆のノイズ除去過程はその繰り返される、逐次的な性質により遅いことです。更に、これらのモデルはピクセル空間で作用するために多くのメモリを消費します、これは高解像度画像を生成するとき巨大になります。そのため、これらのモデルを訓練することと推論のために使用することは困難です。

潜在拡散は、拡散過程を (実際のピクセル空間を使用する代わりに) 低次元の潜在空間に対して適用することにより、メモリと計算の複雑さを軽減することができます。これが標準的な拡散モデルと潜在拡散モデルの主な違いです : 潜在拡散では、モデルは画像の潜在的な (圧縮された) 表現を生成するために訓練されます

潜在拡散には 3 つの主要なコンポーネントがあります。

  1. オートエンコーダ (VAE)
  2. U-Net
  3. テキストエンコーダ, e.g. CLIP のテキストエンコーダ

 
1. オートエンコーダ (VAE)

VAE モデルは 2 つのパート、エンコーダとデコーダを持ちます。エンコーダは画像を低次元潜在表現に変換するために使用され、これは U-Net モデルへの入力として機能します。逆に、デコーダは潜在表現を画像に変換し戻します。

潜在拡散の訓練の間、エンコーダは (各ステップで徐々に多くなるノイズを適用する) 順方向の拡散過程について画像の潜在表現 (latents) を得るために使用されます。推論の間、逆の拡散過程により生成された、ノイズ除去された latents は VAE デコーダを使用して画像に変換し戻されます。(この後) 見るように、推論の間は VAE デコーダだけを必要とします

 
2. U-Net

U-Net はエンコーダ部とデコーダ部を持ち、両方とも ResNet ブロックから構成されます。エンコーダは画像表現を低次元な画像表現に圧縮して、デコーダは低解像度な画像表現を (おそらくはノイズが少ない) 元の、より高解像度な画像表現にデコードし戻します。より具体的には、U-Net 出力はノイズ残差を予測します、これは予測されたノイズ除去された画像表現を計算するために使用できます。

ダウンサンプリングの間に U-Net が重要な情報を失うことを防ぐために、エンコーダのダウンサンプリング ResNet とデコーダのアップサンプリング ResNet 間にショートカット接続が通常は追加されます。更に、stable diffusion U-Net は cross-attention 層を通してその出力をテキスト埋め込み上で条件付けることができます。cross-attention 層は通常は ResNet ブロック間に U-Net のエンコーダ部とデコーダ部の両方に追加されます。

 
3. テキストエンコーダ

テキストエンコーダは例えば “An astronout riding a horse” のような入力プロンプトを U-Net により理解可能な埋め込み空間内に変換することを担います。それは通常は、入力トークンのシークエンスを潜在的なテキスト埋め込みのシークエンスにマップする、単純な transformer ベースのエンコーダです。

Imagen にインスパイアされて、Stable Diffusion は訓練の間にはテキストエンコーダを訓練しないで、単純に CLIP の既に訓練済みのテキストエンコーダ, CLIPTextModel を利用します。

 
潜在拡散は何故高速で効率的か?

潜在拡散は低次元空間で作用するので、ピクセル空間の拡散モデルと比べてそれはメモリと計算要件を大幅に削減します。例えば、Stable Diffusion で使用されるオートエンコーダは 8 の縮小因子 (= reduction factor) を持ちます。これは、shape (3, 512, 512) の画像が潜在空間では (3, 64, 64) になることを意味し、8 × 8 = 64 倍少ないメモリを必要とするだけです。

これが 512 × 512 画像を素早く生成できる理由です、16GB Colab GPU でさえも!

 
推論時の Stable Diffusion

すべてを一つにまとめて、論理的なフローを図示してモデルが推論でどのように動作するか詳しく見ましょう。

stable diffusion モデルは入力として潜在的シードとテキストプロンプトを受け取ります。そして潜在的シードはサイズ $64 \times 64$ のランダムな潜在的画像表現を生成するために使用され、一方でテキストプロンプトは CLIP のテキストエンコーダを使用してサイズ $77 \times 768$ のテキスト埋め込みに変換されます。

次に U-Net は、テキスト埋め込みに条件付けされながら、ランダムな潜在画像表現を反復的にノイズ除去します。ノイズ差分である、U-Net の出力はスケジューラ・アルゴリズムを通してノイズ除去された潜在的画像表現を計算するために使用されます。多くの様々なスケジューラ・アルゴリズムがこの計算のために使用できますが、それぞれに良い点と悪い点があります。Stable Diffusion については、以下の一つの使用を勧めます :

スケジューラ・アルゴリズムがどのように機能するかの理論はこのノートブックの範囲外ですが、手短に言えば、それらは前のノイズ表現と予測されたノイズ差分から予測されたノイズ除去された画像表現を計算することを覚えておくべきです。詳細は、Elucidating the Design Space of Diffusion-Based Generative Models を調べることを勧めます。

ノイズ除去プロセスはより良い潜在的画像表現を段階的に取得するためにおよそ 50 回繰り返されます。完了すれば、潜在的画像表現は変分オートエンコーダのデコーダ部によりデコードされます。

Latent and Stable Diffusion へのこの簡潔なイントロダクションの後は、🤗 Hugging Face Diffusers ライブラリをどのように高度に利用するかを見ましょう!

 

独自の推論パイプラインを書く

最後に、diffusers でカスタム diffusers パイプラインを作成できる方法を示します。カスタム推論パイプラインを書くことは diffusers ライブラリの高度な使用方法で、これは VAE や上述のスケジューラのような特定のコンポーネントに切り替えるために非常に有用な場合があります。

例えば、Stable Diffusion を異なるスケジューラ、つまり (この PR で追加された) Katherine Crowson の K-LMS スケジューラで使用する方法を実演します。

事前訓練済みモデル は完全な diffusion パイプラインをセットアップするために必要な総てのコンポーネントを含みます。それらは以下のフォルダにストアされています :

  • text_encoder : Stable Diffusion は CLIP を使用します が、他の拡散モデルは BERT のような他のエンコーダを使用するかもしれません。
  • tokenizer : text_encoder モデルにより使用されたものと一致していなければなりません。
  • scheduler : 訓練の間に画像にノイズを徐々に追加するために使用されるスケジューリング・アルゴリズムです。
  • unet : 入力の潜在的表現を生成するために使用されるモデル。
  • vae : 潜在的表現を実画像にデコードするために使用するオートエンコーダ・モジュール。

from_pretrained への subfolder 引数を使用し、それらがセーブされているフォルダを参照してコンポーネントをロードできます。

from transformers import CLIPTextModel, CLIPTokenizer
from diffusers import AutoencoderKL, UNet2DConditionModel, PNDMScheduler

# 1. Load the autoencoder model which will be used to decode the latents into image space. 
vae = AutoencoderKL.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="vae", use_auth_token=YOUR_TOKEN)

# 2. Load the tokenizer and text encoder to tokenize and encode the text. 
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14")

# 3. The UNet model for generating the latents.
unet = UNet2DConditionModel.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="unet", use_auth_token=YOUR_TOKEN)

そして事前定義されたスケジューラをロードする代わりに、幾つかの fitting パラメータとともに K-LMS スケジューラ をロードします。

from diffusers import LMSDiscreteScheduler

scheduler = LMSDiscreteScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000)

Next, let’s move the models to GPU.

torch_device = "cuda"
vae.to(torch_device)
text_encoder.to(torch_device)
unet.to(torch_device)

次に画像を生成するために使用するパラメータを定義します。

guidance_scale は Imagen 論文 の式 (2) のガイダンス重み w への類推として定義されていることに注意してください。guidance_scale == 1 は分類器フリーなガイダンスを行わないことに対応します。ここではそれを前に行われたように 7.5 に設定します。

前のサンプルとは違い、より明瞭になった画像を得るために num_inference_steps を 100 に設定します。

prompt = ["a photograph of an astronaut riding a horse"]

height = 512                        # default height of Stable Diffusion
width = 512                         # default width of Stable Diffusion

num_inference_steps = 100           # Number of denoising steps

guidance_scale = 7.5                # Scale for classifier-free guidance

generator = torch.manual_seed(0)    # Seed generator to create the inital latent noise

batch_size = len(prompt)

最初に、渡されたプロンプトに対する text_embeddings を取得します。これらの埋め込みは UNet モデルを条件付けるために使用されて、画像生成を入力プロンプトに似ているはずの何かに向けてガイドします。

text_input = tokenizer(prompt, padding="max_length", max_length=tokenizer.model_max_length, truncation=True, return_tensors="pt")

text_embeddings = text_encoder(text_input.input_ids.to(torch_device))[0]

分類器フリーなガイダンスに対する条件なしのテキスト埋め込みも取得します、これは単にパディングトークン (空テキスト) に対する埋め込みです。それらは条件付き text_embeddings (batch_size と seq_length) と同じ shape を持つ必要があります。

max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
    [""] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
uncond_embeddings = text_encoder(uncond_input.input_ids.to(torch_device))[0]

分類器フリーなガイダンスに対して、2 つのフォワードパスを行なう必要があります : 一つは条件付き入力 (text_embeddings) により、もう一つは条件なし埋め込み (uncond_embeddings) によります。実際には、2 つのフォワードパスを実行することを避けるために両者を単一のバッチに連結することができます。

text_embeddings = torch.cat([uncond_embeddings, text_embeddings])

次に、初期ランダムノイズを生成します。

latents = torch.randn(
    (batch_size, unet.in_channels, height // 8, width // 8),
    generator=generator,
)
latents = latents.to(torch_device)

この段階で潜在変数を調べれば、それらの shape が生成したい画像よりも遥かに小さい torch.Size([1, 4, 64, 64]) であることが分かります。モデルはこの潜在表現 (純粋なノイズ) を後で 512 × 512 画像に変換します。

次に、スケジューラを選択された num_inference_steps で初期化します。これはノイズ除去過程の間に使用される sigmas と正確な時間ステップの値を計算します。

scheduler.set_timesteps(num_inference_steps)

K-LMS スケジューラは latents をその sigma 値で乗算する必要がります。これをここで行いましょう。

latents = latents * scheduler.init_noise_sigma

We are ready to write the denoising loop.

from tqdm.auto import tqdm

scheduler.set_timesteps(num_inference_steps)

for t in tqdm(scheduler.timesteps):
    # expand the latents if we are doing classifier-free guidance to avoid doing two forward passes.
    latent_model_input = torch.cat([latents] * 2)

    latent_model_input = scheduler.scale_model_input(latent_model_input)

    # predict the noise residual
    with torch.no_grad():
        noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample

    # perform guidance
    noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
    noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)

    # compute the previous noisy sample x_t -> x_t-1
    latents = scheduler.step(noise_pred, t, latents).prev_sample

そして生成された latents を画像にデコードし戻すために vae を使用します。

# scale and decode the image latents with vae
latents = 1 / 0.18215 * latents
image = vae.decode(latents).sample

そして最後に、画像を PIL に変換しましょう、それを表示またはセーブできるように。

image = (image / 2 + 0.5).clamp(0, 1)
image = image.detach().cpu().permute(0, 2, 3, 1).numpy()
images = (image * 255).round().astype("uint8")
pil_images = [Image.fromarray(image) for image in images]
pil_images[0]

 

以上