Stable Diffusion是一個(gè)強(qiáng)大的文本條件隱式擴(kuò)散模型(text-conditioned latent diffusion model),它具有根據(jù)文字描述生成精美圖片的能力。它不僅是一個(gè)完全開(kāi)源的模型(代碼,數(shù)據(jù),模型全部開(kāi)源),而且是它的參數(shù)量只有
1B
左右,大部分人可以在普通的顯卡上進(jìn)行推理甚至精調(diào)模型。毫不夸張的說(shuō),Stable Diffusion的出現(xiàn)和開(kāi)源對(duì)AIGC的火熱和發(fā)展是有巨大推動(dòng)作用的,因?yàn)樗尭嗟娜四芸斓厣鲜諥I作畫(huà)。本文將基于Hugging Face的diffusers庫(kù)深入講解Stable Diffusion的技術(shù)原理以及部分的實(shí)現(xiàn)細(xì)節(jié),然后也會(huì)介紹Stable Diffusion的常用功能。
1. Stable Diffusion簡(jiǎn)介
Stable Diffusion是CompVis、Stability AI和LAION等公司研發(fā)的一個(gè)文生圖模型,它的模型和代碼是開(kāi)源的,而且訓(xùn)練數(shù)據(jù)LAION-5B
也是開(kāi)源的。
Stable Diffusion是一個(gè)基于Latent的擴(kuò)散模型,它在UNet中引入text condition來(lái)實(shí)現(xiàn)基于文本生成圖像。Stable Diffusion的核心來(lái)源于Latent Diffusion這個(gè)工作,常規(guī)的擴(kuò)散模型是基于Pixel(像素)的生成模型,而Latent Diffusion是基于Latent的生成模型,它先采用一個(gè)autoencoder將圖像壓縮到Latent空間,然后用擴(kuò)散模型來(lái)生成圖像的Latents,最后送入autoencoder的decoder模塊就可以得到生成的圖像。
Stable Diffusion模型的主體結(jié)構(gòu)如下圖所示,主要包括三個(gè)模型:
-
AutoEncoder
:Encoder將圖像壓縮到Latent空間,而Decoder將Latent解碼為圖像; -
CLIP text encoder
:提取輸入text的text embeddings,通過(guò)cross attention方式送入擴(kuò)散模型的UNet中作為condition; -
UNet
:擴(kuò)散模型的主體,用來(lái)實(shí)現(xiàn)文本引導(dǎo)下的Latent生成。
對(duì)于Stable Diffusion模型,其Autoencoder模型參數(shù)大小為84M,CLIP text encoder模型大小為123M,而UNet參數(shù)大小為860M,所以Stable Diffusion模型的總參數(shù)量約為1B。
基于Latent的擴(kuò)散模型的優(yōu)勢(shì)在于計(jì)算效率更高效,因?yàn)閳D像的Latent空間要比圖像Pixel空間要小,這也是Stable Diffusion的核心優(yōu)勢(shì)
。文生圖模型往往參數(shù)量比較大,基于Pixel的方法往往限于算力只生成64x64大小的圖像,比如OpenAI的DALL-E2和谷歌的Imagen,然后再通過(guò)超分辨模型將圖像分辨率提升至256x256和1024x1024;而基于Latent的SD是在Latent空間操作的,它可以直接生成256x256和512x512甚至更高分辨率的圖像。
1.1 基本概念
-
隱式擴(kuò)散
:使用VAE(Variational Auto-Encoder)將圖片映射到一個(gè)較小的隱式表征,再將其映射到原始圖片,通過(guò)在隱式表征上進(jìn)行擴(kuò)散,可以使用更少的內(nèi)存,減少UNet層數(shù)并加速圖片的生成。還可以將結(jié)果輸入VAE解碼器中,得到高分辨率圖像。Stable Diffusion中的VAE能夠接收一張三通道圖片作為輸入,從而生成一個(gè)四通道的隱式表征,同時(shí)每一個(gè)空間維度都將減少為原來(lái)的八分之一。 -
以文本為生成條件
:在推理階段,輸入期望圖像的文本描述,將純?cè)肼晹?shù)據(jù)作為起點(diǎn),然后模型對(duì)噪聲輸入進(jìn)行“去噪”,生成能匹配文本描述的圖像。為此,Stable Diffusion使用了一個(gè)名為CLIP的預(yù)訓(xùn)練Transformer
模型。- CLIP的文本編碼器將文本描述轉(zhuǎn)換為特征向量,該特征向量用于與圖像特征向量進(jìn)行相似度比較。
- 輸入的文本提示語(yǔ)進(jìn)行分詞(也就是基于一個(gè)很大的詞匯庫(kù),將句子中的詞語(yǔ)或短語(yǔ)轉(zhuǎn)換為一個(gè)一個(gè)的token),然后被輸入CLIP的文本編碼器。
- 使用交叉注意力機(jī)制(cross attention),交叉注意力貫穿整個(gè)UNet結(jié)構(gòu),UNet中的每個(gè)空間位置都可以“注意”到文字條件中不同的token,以便從文本提示語(yǔ)中獲取不同位置的相互關(guān)聯(lián)信息。
下圖展示了文本條件信息(以及基于時(shí)間步的條件)是如何在不同位置被輸入的。
文本條件信息通過(guò)交叉注意力被輸入到各個(gè)模塊,時(shí)間步信息通過(guò)時(shí)間嵌入的映射被輸入到各個(gè)模塊。
-
無(wú)分類(lèi)器引導(dǎo)
(Classifier-Free Guidance, CFG):主要解決可能得到與文字描述根本不相關(guān)的圖片,具體方法如下:- 訓(xùn)練階段,強(qiáng)制模型學(xué)習(xí)在無(wú)文字信息的情況下對(duì)圖片“去噪”(無(wú)條件生成)。
- 推理階段,進(jìn)行有文字條件預(yù)測(cè)vs無(wú)文字條件預(yù)測(cè),利用兩者的差異來(lái)建立一個(gè)最終結(jié)合版的預(yù)測(cè)。
1.2 主體結(jié)構(gòu)
(1)AutoEncoder
AutoEncoder是一個(gè)基于encoder-decoder
架構(gòu)的圖像壓縮模型,對(duì)于一個(gè)大小為
H
×
W
×
3
H\times W\times 3
H×W×3的輸入圖像,encoder模塊將其編碼為一個(gè)大小為
h
×
w
×
c
h\times w\times c
h×w×c的Latent,其中
f
=
H
/
h
=
W
/
h
f=H/h=W/h
f=H/h=W/h為下采樣率(downsampling factor)。
在訓(xùn)練AutoEncoder過(guò)程中,除了采用L1重建損失外,還增加了感知損失(Perceptual Loss,即LPIPS,具體見(jiàn)論文The Unreasonable Effectiveness of Deep Features as a Perceptual Metric)以及基于Patch的對(duì)抗訓(xùn)練。
輔助Loss主要是為了確保重建的圖像局部真實(shí)性以及避免模糊,具體損失函數(shù)見(jiàn)Latent Diffusion的Loss部分。同時(shí)為了防止得到的latent的標(biāo)準(zhǔn)差過(guò)大,采用了兩種正則化方法:
- 第一種是KL-reg,類(lèi)似VAE增加一個(gè)latent和標(biāo)準(zhǔn)正態(tài)分布的KL loss,不過(guò)這里為了保證重建效果,采用比較小的權(quán)重(~10e-6);
- 第二種是VQ-reg,引入一個(gè)VQ (vector quantization)layer,此時(shí)的模型可以看成是一個(gè)VQ-GAN,不過(guò)VQ層是在decoder模塊中,這里VQ的codebook采樣較高的維度(8192)來(lái)降低正則化對(duì)重建效果的影響。
Stable Diffusion采用基于KL-reg的autoencoder,其中下采樣率 f = 8 f=8 f=8,特征維度為 c = 4 c=4 c=4,當(dāng)輸入圖像為512x512大小時(shí)將得到64x64x4大小的Latent。AutoEncoder模型時(shí)在OpenImages數(shù)據(jù)集上基于256x256大小訓(xùn)練的,但是由于AutoEncoder的模型是全卷積結(jié)構(gòu)的(基于ResnetBlock),所以它可以擴(kuò)展應(yīng)用在尺寸>256的圖像上。
下面給出使用diffusers庫(kù)來(lái)加載AutoEncoder模型,并使用AutoEncoder來(lái)實(shí)現(xiàn)圖像的壓縮和重建的代碼:
import torch
import numpy as np
from diffusers.models import AutoencoderKL
from diffusers import StableDiffusionPipeline
from PIL import Image
# 加載模型
autoencoder = AutoencoderKL.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="vae")
autoencoder.to("cuda", dtype=torch.float16)
# 讀取圖像并預(yù)處理
raw_image = Image.open("boy.jpg").convert("RGB").resize((256, 256))
image = np.array(raw_image).astype(np.float32) / 127.5 - 1.0
image = image[None].transpose(0, 3, 1, 2)
image = torch.from_numpy(image)
# 壓縮圖像為L(zhǎng)atent并重建
with torch.inference_mode():
latent = autoencoder.encode(image.to("cuda", dtype=torch.float16)).latent_dist.sample()
rec_image = autoencoder.decode(latent).sample
rec_image = (rec_image/2 + 0.5).clamp(0, 1)
rec_image = rec_image.cpu().permute(0,2,3,1).numpy()
rec_image = (rec_image * 255).round().astype("uint8")
rec_image = Image.fromarray(rec_image[0])
rec_image
由于Stable Diffusion采用的AutoEncoder是基于KL-reg的,所以這個(gè)AutoEncoder在編碼圖像時(shí)其實(shí)得到的是一個(gè)高斯分布DiagonalGaussianDistribution
(分布的均值和標(biāo)準(zhǔn)差),然后通過(guò)調(diào)用sample方法來(lái)采樣一個(gè)具體的Latent(調(diào)用mode方法可以得到均值)。
由于KL-reg
的權(quán)重系數(shù)非常小,實(shí)際得到Latent的標(biāo)準(zhǔn)差還是比較大的,Latent diffusion論文中提出了一種rescaling
方法:首先計(jì)算出第一個(gè)batch數(shù)據(jù)中的latent的標(biāo)準(zhǔn)差
σ
^
\hat{\sigma}
σ^,然后采用
1
/
σ
^
1/\hat{\sigma}
1/σ^的系數(shù)來(lái)rescale latent,這樣就盡量保證latent的標(biāo)準(zhǔn)差接近1(防止擴(kuò)散過(guò)程的SNR較高,影響生成效果),然后擴(kuò)散模型也是應(yīng)用在rescaling的latent上,在解碼時(shí)只需要將生成的latent除以
1
/
σ
^
1/\hat{\sigma}
1/σ^,然后再送入autoencoder的decoder即可。對(duì)于SD所使用的autoencoder,這個(gè)rescaling系數(shù)為0.18215。
(2)CLIP text encoder
Stable Diffusion采用CLIP text encoder來(lái)對(duì)輸入text提取text embeddings,具體的是采用目前OpenAI所開(kāi)源的最大CLIP模型:clip-vit-large-patch14
,這個(gè)CLIP的text encoder是一個(gè)transformer
模型(只有encoder模塊):層數(shù)為12,特征維度為768,模型參數(shù)大小是123M。
對(duì)于輸入text,送入CLIP text encoder
后得到最后的hidden states
(即最后一個(gè)transformer block得到的特征),其特征維度大小為77x768(77是token的數(shù)量),這個(gè)細(xì)粒度的text embeddings將以cross attention的方式送入U(xiǎn)Net中。在transofmers
庫(kù)中,可以如下使用CLIP text encoder:
from transformers import CLIPTextModel, CLIPTokenizer
text_encoder = CLIPTextModel.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="text_encoder").to("cuda")
# text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14").to("cuda")
text_tokenizer = CLIPTokenizer.from_pretrained("runwayml/stable-diffusion-v1-5", subfolder="tokenizer")
# tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
# 對(duì)輸入的text進(jìn)行tokenize,得到對(duì)應(yīng)的token ids
prompt = "a photograph of an astronaut riding a horse"
text_input_ids = text_tokenizer(
prompt,
padding="max_length",
max_length=text_tokenizer.model_max_length,
truncation=True,
return_tensors="pt"
).input_ids
# 將token ids送入text model得到77x768的特征
text_embeddings = text_encoder(text_input_ids.to("cuda"))[0]
值得注意的是,這里的text_tokenizer最大長(zhǎng)度為77(CLIP訓(xùn)練時(shí)所采用的設(shè)置),當(dāng)輸入text的tokens數(shù)量超過(guò)77后,將進(jìn)行截?cái)?/strong>,如果不足則進(jìn)行paddings,這樣將保證無(wú)論輸入任何長(zhǎng)度的文本(甚至是空文本)都得到77x768大小的特征。而且在訓(xùn)練Stable Diffusion的過(guò)程中,CLIP text encoder模型是凍結(jié)的。
補(bǔ)充
在早期的工作中,比如OpenAI的GLIDE
和latent diffusion
中的LDM均采用一個(gè)隨機(jī)初始化的tranformer
模型來(lái)提取text的特征,但是最新的工作都是采用預(yù)訓(xùn)練好的text model。比如谷歌的Imagen
采用純文本模型T5 encoder
來(lái)提出文本特征,而Stable Diffusion則采用CLIP text encoder
,預(yù)訓(xùn)練好的模型往往已經(jīng)在大規(guī)模數(shù)據(jù)集上進(jìn)行了訓(xùn)練,它們要比直接采用一個(gè)從零訓(xùn)練好的模型要好。
(3)UNet
Stable Diffusion的擴(kuò)散模型是一個(gè)860M的UNet
,其主要結(jié)構(gòu)如下圖所示(這里以輸入的Latent為64x64x4維度為例),
其中encoder部分包括3個(gè)CrossAttnDownBlock2D模塊和1個(gè)DownBlock2D模塊,而decoder部分包括1個(gè)UpBlock2D模塊和3個(gè)CrossAttnUpBlock2D模塊,中間還有一個(gè)UNetMidBlock2DCrossAttn模塊。encoder和decoder兩個(gè)部分是完全對(duì)應(yīng)的,中間存在skip connection。注意3個(gè)CrossAttnDownBlock2D模塊最后均有一個(gè)2x的downsample操作,而DownBlock2D模塊是不包含下采樣的。
其中CrossAttnDownBlock2D
模塊的主要結(jié)構(gòu)如下圖所示,text condition
將通過(guò)CrossAttention
模塊嵌入進(jìn)來(lái),此時(shí)Attention的query是UNet的中間特征,而key和value則是text embeddings。
Stable Diffusion和DDPM一樣采用預(yù)測(cè)noise的方法來(lái)訓(xùn)練UNet,其訓(xùn)練損失也和DDPM一樣:
L
s
i
m
p
l
e
=
E
x
0
,
?
~
N
(
0
,
I
)
,
t
[
∣
∣
?
?
?
θ
(
α
ˉ
t
x
0
+
1
?
α
ˉ
t
?
,
t
,
c
)
∣
∣
2
]
L^{simple}=\mathbb{E}_{x_0,\epsilon \sim \mathcal{N}(0,\mathrm{I}),t}[||\epsilon -\epsilon _{\theta }(\sqrt{\bar{\alpha }_t}\mathrm{x}_0+\sqrt{1-\bar{\alpha }_t}\epsilon,t,\mathrm{c})||^2]
Lsimple=Ex0?,?~N(0,I),t?[∣∣???θ?(αˉt??x0?+1?αˉt???,t,c)∣∣2]
這里的
c
c
c為text embeddings,此時(shí)的模型是一個(gè)條件擴(kuò)散模型。
在訓(xùn)練條件擴(kuò)散模型時(shí),往往會(huì)采用Classifier-Free Guidance(簡(jiǎn)稱(chēng)為CFG),所謂的CFG簡(jiǎn)單來(lái)說(shuō)就是在訓(xùn)練條件擴(kuò)散模型的同時(shí)也訓(xùn)練一個(gè)無(wú)條件的擴(kuò)散模型,同時(shí)在采樣階段將條件控制下預(yù)測(cè)的噪音和無(wú)條件下的預(yù)測(cè)噪音組合在一起來(lái)確定最終的噪音,具體的計(jì)算公式如下所示:
這里的
w
w
w為guidance scale
,當(dāng)
w
w
w越大時(shí),condition起的作用越大,即生成的圖像其更和輸入文本一致。CFG的具體實(shí)現(xiàn)非常簡(jiǎn)單,在訓(xùn)練過(guò)程中,我們只需要以一定的概率(比如10%)隨機(jī)drop掉text即可,這里我們可以將text置為空字符串(前面說(shuō)過(guò)此時(shí)依然能夠提取text embeddings)。
1.3 訓(xùn)練細(xì)節(jié)
Stable Diffusion的訓(xùn)練主要包括訓(xùn)練數(shù)據(jù)和訓(xùn)練資源,這方面也是在Stable Diffusion的Model Card上有說(shuō)明。首先是訓(xùn)練數(shù)據(jù),Stable Diffusion在laion2B-en數(shù)據(jù)集上訓(xùn)練的,它是laion-5b數(shù)據(jù)集的一個(gè)子集,更具體的說(shuō)它是laion-5b中的英文(文本為英文)數(shù)據(jù)集。laion-5b數(shù)據(jù)集是從網(wǎng)頁(yè)數(shù)據(jù)Common Crawl中篩選出來(lái)的圖像-文本對(duì)數(shù)據(jù)集,它包含5.85B的圖像-文本對(duì),其中文本為英文的數(shù)據(jù)量為2.32B,這就是laion2B-en數(shù)據(jù)集。
下面是laion2B-en數(shù)據(jù)集的元信息(圖片width和height,以及文本長(zhǎng)度)統(tǒng)計(jì)分析:
其中圖片的width和height均在256以上的樣本量為1324M,在512以上的樣本量為488M,而在1024以上的樣本為76M;文本的平均長(zhǎng)度為67。Laion數(shù)據(jù)集中除了圖片(下載URL,圖像width和height)和文本(描述文本)的元信息外,還包含以下信息:
-
similarity
:使用CLIP ViT-B/32計(jì)算出來(lái)的圖像和文本余弦相似度; -
pwatermark
:使用一個(gè)圖片水印檢測(cè)器檢測(cè)的概率值,表示圖片含有水印的概率; -
punsafe
:圖片是否安全,或者圖片是不是NSFW,使用基于CLIP的檢測(cè)器來(lái)估計(jì); -
AESTHETIC_SCORE
:圖片的美學(xué)評(píng)分(1-10),這個(gè)是后來(lái)追加的,首先選擇一小部分圖片數(shù)據(jù)集讓人對(duì)圖片的美學(xué)打分,然后基于這個(gè)標(biāo)注數(shù)據(jù)集來(lái)訓(xùn)練一個(gè)打分模型,并對(duì)所有樣本計(jì)算估計(jì)的美學(xué)評(píng)分。
上面是Laion數(shù)據(jù)集的情況,下面介紹Stable Diffusion訓(xùn)練數(shù)據(jù)集的具體情況,Stable Diffusion的訓(xùn)練是多階段的(先在256x256尺寸上預(yù)訓(xùn)練,然后在512x512尺寸上精調(diào)),不同的階段產(chǎn)生了不同的版本:
- SD v1.1:在Laion2B-en數(shù)據(jù)集上以256x256大小訓(xùn)練237,000步,如上所述,laion2B-en數(shù)據(jù)集中256以上的樣本量共1324M;然后在Laion5B的高分辨率數(shù)據(jù)集以512x512尺寸訓(xùn)練194,000步,這里的高分辨率數(shù)據(jù)集是圖像尺寸在1024x1024以上,共170M樣本。
- SD v1.2:以SD v1.1為初始權(quán)重,在improved_aesthetics_5plus數(shù)據(jù)集上以512x512尺寸訓(xùn)練515,000步數(shù),這個(gè)improved_aesthetics_5plus數(shù)據(jù)集上laion2B-en數(shù)據(jù)集中美學(xué)評(píng)分在5分以上的子集(共約600M樣本),注意這里過(guò)濾了含有水印的圖片(pwatermark>0.5)以及圖片尺寸在512x512以下的樣本。
- SD v1.3:以SD v1.2為初始權(quán)重,在improved_aesthetics_5plus數(shù)據(jù)集上繼續(xù)以512x512尺寸訓(xùn)練195,000步數(shù),不過(guò)這里采用了CFG(以10%的概率隨機(jī)drop掉text)。
- SD v1.4:以SD v1.2為初始權(quán)重,在improved_aesthetics_5plus數(shù)據(jù)集上采用CFG以512x512尺寸訓(xùn)練225,000步數(shù)。
- SD v1.5:以SD v1.2為初始權(quán)重,在improved_aesthetics_5plus數(shù)據(jù)集上采用CFG以512x512尺寸訓(xùn)練595,000步數(shù)。
可以看到SD v1.3、SD v1.4和SD v1.5其實(shí)是以SD v1.2為起點(diǎn)在improved_aesthetics_5plus數(shù)據(jù)集上采用CFG訓(xùn)練過(guò)程中的不同checkpoints,目前最常用的版本是SD v1.4和SD v1.5。SD的訓(xùn)練是采用了32臺(tái)8卡的A100機(jī)器(32 x 8 x A100_40GB GPUs),所需要的訓(xùn)練硬件還是比較多的??梢院?jiǎn)單計(jì)算一下,單卡的訓(xùn)練batch size為2,并采用gradient accumulation,其中g(shù)radient accumulation steps=2,那么訓(xùn)練的總batch size就是32x8x2x2=2048。訓(xùn)練優(yōu)化器采用AdamW,訓(xùn)練采用warmup,在初始10,000步后學(xué)習(xí)速率升到0.0001,后面保持不變。至于訓(xùn)練時(shí)間,文檔上只說(shuō)了用了150,000小時(shí),這個(gè)應(yīng)該是A100卡時(shí),如果按照256卡A100來(lái)算的話,那么大約需要訓(xùn)練25天左右。
1.4 模型評(píng)測(cè)
對(duì)于文生圖模型,目前常采用的定量指標(biāo)是FID
(Fréchet inception distance)和CLIP score
,其中FID可以衡量生成圖像的逼真度(image fidelity),而CLIP score評(píng)測(cè)的是生成的圖像與輸入文本的一致性,其中FID越低越好,而CLIP score是越大越好。當(dāng)CFG的gudiance scale參數(shù)設(shè)置不同時(shí),F(xiàn)ID和CLIP score會(huì)發(fā)生變化,下圖為不同的gudiance scale參數(shù)下,SD模型在COCO2017驗(yàn)證集上的評(píng)測(cè)結(jié)果,注意這里是zero-shot
評(píng)測(cè),即SD模型并沒(méi)有在COCO訓(xùn)練數(shù)據(jù)集上精調(diào)。
可以看到當(dāng)gudiance scale=3時(shí),F(xiàn)ID最低;而當(dāng)gudiance scale越大時(shí),CLIP score越大,但是FID同時(shí)也變大。在實(shí)際應(yīng)用時(shí),往往會(huì)采用較大的gudiance scale,比如SD模型默認(rèn)采用7.5,此時(shí)生成的圖像和文本有較好的一致性。
從不同版本的對(duì)比曲線上看,Stable Diffusion的采用CFG訓(xùn)練后三個(gè)版本其實(shí)差別并沒(méi)有那么大,其中SD v1.5相對(duì)好一點(diǎn),但是明顯要未采用CFG訓(xùn)練的版本要好的多,這說(shuō)明CFG訓(xùn)練是比較關(guān)鍵的。
但是FID有很大的局限性,它并不能很好地衡量生成圖像的質(zhì)量,也是因?yàn)檫@個(gè)原因,谷歌的Imagen引入了人工評(píng)價(jià),先建立一個(gè)評(píng)測(cè)數(shù)據(jù)集DrawBench
(包含200個(gè)不同類(lèi)型的text),然后用不同的模型來(lái)生成圖像,讓人去評(píng)價(jià)同一個(gè)text下不同模型生成的圖像,這種評(píng)測(cè)方式比較直接,但是可能也受一些主觀因素的影響。
1.5 模型應(yīng)用
(1)文生圖
根據(jù)文本生成圖像這是文生圖的最核心的功能,下圖為Stable Diffusion的文生圖的推理流程圖:
- 首先,根據(jù)輸入text用text encoder提取text embeddings,同時(shí)初始化一個(gè)隨機(jī)噪音noise(latent上的,512x512圖像對(duì)應(yīng)的noise維度為64x64x4),
- 然后,將text embeddings和noise送入擴(kuò)散模型UNet中生成去噪后的latent,
- 最后,送入autoencoder的decoder模塊得到生成的圖像。
使用diffusers
庫(kù),可以直接調(diào)用StableDiffusionPipeline
來(lái)實(shí)現(xiàn)文生圖:
import torch
from diffusers import StableDiffusionPipeline
from PIL import Image
# 判斷當(dāng)前的設(shè)備
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using device: {device}')
# 組合圖像,生成grid
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
# 加載文生圖pipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", # 或者使用 SD v1.4: "CompVis/stable-diffusion-v1-4"
torch_dtype=torch.float16
).to(device)
# 輸入text,這里text又稱(chēng)為prompt
prompts = [
"a photograph of an astronaut riding a horse",
"A cute otter in a rainbow whirlpool holding shells, watercolor",
"An avocado armchair",
"A white dog wearing sunglasses"
]
# 定義隨機(jī)seed,保證可重復(fù)性
generator = torch.Generator(device).manual_seed(42)
# 執(zhí)行推理
images = pipe(
prompts,
height=512,
width=512,
num_inference_steps=50,
guidance_scale=7.5,
negative_prompt=None,
num_images_per_prompt=1,
generator=generator
).images
grid = image_grid(images, rows=1, cols=4)
grid
生成的圖像效果如下所示:
可以通過(guò)指定width
和height
來(lái)決定生成圖像的大小,前面提到Stable Diffusion最后是在512x512尺度上訓(xùn)練的,所以生成512x512尺寸效果是最好的,但是實(shí)際上Stable Diffusion可以生成任意尺寸的圖片:一方面AutoEncoder支持任意尺寸的圖片的編碼和解碼,另外一方面擴(kuò)散模型UNet也是支持任意尺寸的latents生成的(UNet是卷積+attention的混合結(jié)構(gòu))。
但是注意生成低分辨率圖像時(shí),圖像的質(zhì)量會(huì)大幅度下降,這是因?yàn)?strong>訓(xùn)練是在固定尺寸上(512x512)進(jìn)行的,生成其它尺寸圖像還是會(huì)存在一定的問(wèn)題。解決這個(gè)問(wèn)題的相對(duì)比較簡(jiǎn)單,就是采用多尺度策略訓(xùn)練,比如NovelAI提出采用Aspect Ratio Bucketing
策略來(lái)在二次元數(shù)據(jù)集上精調(diào)模型,這樣得到的模型就很大程度上避免Stable Diffusion的這個(gè)問(wèn)題,目前大部分開(kāi)源的基于Stable Diffusion的精調(diào)模型往往都采用類(lèi)似的多尺度策略來(lái)精調(diào)。
這里的另一個(gè)比較重要的參數(shù)是num_inference_steps
,它是指推理過(guò)程中的去噪步數(shù)或者采樣步數(shù)。Stable Diffusion在訓(xùn)練過(guò)程采用的是步數(shù)為1000的noise scheduler,但是在推理時(shí)往往采用速度更快的scheduler:只需要少量的采樣步數(shù)就能生成不錯(cuò)的圖像,比如Stable Diffusion默認(rèn)采用PNDM scheduler
,它只需要采樣50步就可以出圖。當(dāng)然我們也可以換用其它類(lèi)型的scheduler,比如DDIM scheduler
和DPM-Solver scheduler
。
可以在diffusers中直接替換scheduler,比如使用DDIM:
from diffusers import DDIMScheduler
# 注意這里的clip_sample要關(guān)閉,否則生成圖像存在問(wèn)題,因?yàn)椴荒軐?duì)latent進(jìn)行clip
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config, clip_sample=False)
換成DDIM后,同樣的采樣步數(shù)生成的圖像如下所示,在部分細(xì)節(jié)上和PNDM有差異:
當(dāng)然采樣步數(shù)越大,生成的圖像質(zhì)量越好,但是相應(yīng)的推理時(shí)間也更久。
第三個(gè)要討論的參數(shù)是guidance_scale
,當(dāng)CFG的guidance_scale
越大時(shí),生成的圖像應(yīng)該會(huì)和輸入文本更一致,但是過(guò)大的guidance_scale
會(huì)出現(xiàn)問(wèn)題,這主要是由于訓(xùn)練和測(cè)試的不一致,過(guò)大的guidance_scale會(huì)導(dǎo)致生成的樣本超出范圍。谷歌的Imagen
論文提出一種dynamic thresholding
策略來(lái)解決這個(gè)問(wèn)題,所謂的dynamic thresholding是相對(duì)于原來(lái)的static thresholding:
-
static thresholding策略
是直接將生成的樣本clip到[-1, 1]范圍內(nèi)(Imagen是基于pixel的擴(kuò)散模型,這里是將圖像像素值歸一化到-1到1之間),但是會(huì)在過(guò)大的guidance_scale時(shí)產(chǎn)生很多的飽含像素點(diǎn)。 -
dynamic thresholding策略
是先計(jì)算樣本在某個(gè)百分位下(比如99%)的像素絕對(duì)值 s s s,然后如果它超過(guò)1時(shí)就采用 s s s來(lái)進(jìn)行clip,這樣就可以大大減少過(guò)飽和的像素。
這兩種策略的具體代碼如下:
dynamic thresholding策略對(duì)于Imagen是比較關(guān)鍵的,它使得Imagen可以采用較大的guidance_scale來(lái)生成更自然的圖像。
另一個(gè)比較容易忽略的參數(shù)是negative_prompt
,這個(gè)參數(shù)和CFG有關(guān),前面說(shuō)過(guò)Stable Diffusion采用了CFG來(lái)提升生成圖像的質(zhì)量。使用CFG,去噪過(guò)程的噪音預(yù)測(cè)不僅僅依賴(lài)條件擴(kuò)散模型,也依賴(lài)無(wú)條件擴(kuò)散模型:
KaTeX parse error: Expected 'EOF', got '_' at position 11: \text{pred_?noise}=w\epsilo…
這里的negative_prompt為無(wú)條件擴(kuò)散模型的text輸入,上文說(shuō)到訓(xùn)練過(guò)程中將text置為空字符串來(lái)實(shí)現(xiàn)無(wú)條件擴(kuò)散模型,即negative_prompt = None = ""
。但是有時(shí)候可以使用不為空的negative_prompt來(lái)避免模型生成的圖像包含不想要的東西,因?yàn)閺纳鲜龉娇梢钥吹竭@里的無(wú)條件擴(kuò)散模型是想遠(yuǎn)離的部分。
合理使用negative prompt
能夠幫助去除不想要的東西來(lái)提升圖像生成效果。 一般情況下,輸入的text或者prompt我們稱(chēng)之為“正向提示詞”,而negative prompt稱(chēng)之為“反向提示詞”,想要生成的好的圖像,不僅要選擇好的正向提示詞,也需要好的反向提示詞,這和文本生成模型也比較類(lèi)似:都需要好的prompt。
一個(gè)對(duì)正向prompt優(yōu)化的例子:
-
原始prompt為"A rabbit is wearing a space suit",直接生成的效果:
-
將prompt改為"A rabbit is wearing a space suit, digital Art, Greg rutkowski, Trending cinematographic artstation",其生成的效果就大大提升:
(2)圖生圖
圖生圖(image2image)是對(duì)文生圖功能的一個(gè)擴(kuò)展,其核心思路也非常簡(jiǎn)單:給定一個(gè)筆畫(huà)的色塊圖像,可以先給它加一定的高斯噪音(執(zhí)行擴(kuò)散過(guò)程)得到噪音圖像,然后基于擴(kuò)散模型對(duì)這個(gè)噪音圖像進(jìn)行去噪,就可以生成新的圖像,但是這個(gè)圖像在結(jié)構(gòu)和布局和輸入圖像基本一致。
對(duì)于Stable Diffusion來(lái)說(shuō),圖生圖的流程圖如下所示,相比文生圖流程來(lái)說(shuō),這里的初始latent不再是一個(gè)隨機(jī)噪音,而是由初始圖像經(jīng)過(guò)autoencoder編碼之后的latent加高斯噪音得到,這里的加噪過(guò)程就是擴(kuò)散過(guò)程。要注意的是,去噪過(guò)程的步數(shù)要和加噪過(guò)程的步數(shù)一致,就是說(shuō)你加了多少噪音,就應(yīng)該去掉多少噪音,這樣才能生成想要的無(wú)噪音圖像。
在diffusers中,可以使用StableDiffusionImg2ImgPipeline
來(lái)實(shí)現(xiàn)文生圖,具體代碼如下:
import torch
from diffusers import StableDiffusionImg2ImgPipeline
from PIL import Image
# 加載圖生圖pipeline
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id, torch_dtype=torch.float32).to(device)
# 讀取初始圖片
init_image = Image.open("boy.jpg").convert("RGB")
# 推理
prompt = "A fantasy landscape, trending on artstation"
generator = torch.Generator(device=device).manual_seed(2023)
image = pipe(
prompt=prompt,
image=init_image,
strength=0.8,
guidance_scale=7.5,
generator=generator
).images[0]
image
相比文生圖的pipeline,圖生圖的pipeline還多了一個(gè)參數(shù)strength,這個(gè)參數(shù)介于0-1之間,表示對(duì)輸入圖片加噪音的程度,這個(gè)值越大加的噪音越多,對(duì)原始圖片的破壞也就越大,當(dāng)strength=1時(shí),其實(shí)就變成了一個(gè)隨機(jī)噪音,此時(shí)就相當(dāng)于純粹的文生圖pipeline了。
總結(jié)來(lái)看,圖生圖其實(shí)核心也是依賴(lài)了文生圖的能力,其中strength這個(gè)參數(shù)需要靈活調(diào)節(jié)來(lái)得到滿(mǎn)意的圖像。
(3)圖像修補(bǔ)
圖像修補(bǔ),image inpainting
,它和圖生圖一樣也是文生圖功能的一個(gè)擴(kuò)展。Stable Diffusion的圖像inpainting不是用在圖像修復(fù)上,而是主要用在圖像編輯上:給定一個(gè)輸入圖像和想要編輯的區(qū)域mask,通過(guò)文生圖來(lái)編輯mask區(qū)域的內(nèi)容。Stable Diffusion的圖像inpainting原理圖如下所示:
和圖生圖一樣:首先將輸入圖像通過(guò)autoencoder編碼為latent,然后加入一定的高斯噪音生成noisy latent,再進(jìn)行去噪生成圖像,但是這里為了保證mask以外的區(qū)域不發(fā)生變化,在去噪過(guò)程的每一步,都將擴(kuò)散模型預(yù)測(cè)的noisy latent用真實(shí)圖像同level的nosiy latent替換。
在diffusers中,使用StableDiffusionInpaintPipelineLegacy
可以實(shí)現(xiàn)文本引導(dǎo)下的圖像inpainting,具體代碼如下所示:
import torch
from diffusers import StableDiffusionInpaintPipelineLegacy
from PIL import Image
# 加載inpainting pipeline
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda")
# 讀取輸入圖像和輸入mask
input_image = Image.open("overture-creations-5sI6fQgYIuo.png").resize((512, 512))
input_mask = Image.open("overture-creations-5sI6fQgYIuo_mask.png").resize((512, 512))
# 執(zhí)行推理
prompt = ["a mecha robot sitting on a bench", "a cat sitting on a bench"]
generator = torch.Generator("cuda").manual_seed(0)
with torch.autocast("cuda"):
images = pipe(
prompt=prompt,
image=input_image,
mask_image=input_mask,
num_inference_steps=50,
strength=0.75,
guidance_scale=7.5,
num_images_per_prompt=1,
generator=generator,
).images
下面是一個(gè)具體的生成效果,這里我們將輸入圖像的dog換成了mecha robot或者cat,從而實(shí)現(xiàn)了圖像編輯。
要注意的是這里的參數(shù)guidance_scale也和圖生圖一樣比較重要,要生成好的圖像,需要選擇合適的guidance_scale。如果guidance_scale=0.5時(shí),生成的圖像由于過(guò)于受到原圖干擾而產(chǎn)生一些不協(xié)調(diào)。
無(wú)論是上面的圖生圖還是這里的圖像inpainting,其實(shí)并沒(méi)有去finetune Stable Diffusion模型,只是擴(kuò)展了它的能力,但是這兩樣功能就需要精確調(diào)整參數(shù)才能得到滿(mǎn)意的生成效果。 這里也給出StableDiffusionInpaintPipelineLegacy
這個(gè)pipeline內(nèi)部的核心代碼:
import PIL
import numpy as np
import torch
from diffusers import AutoencoderKL, UNet2DConditionModel, DDIMScheduler
from transformers import CLIPTextModel, CLIPTokenizer
from tqdm.auto import tqdm
def preprocess_mask(mask):
mask = mask.convert("L")
w, h = mask.size
w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
mask = mask.resize((w // 8, h // 8), resample=PIL.Image.NEAREST)
mask = np.array(mask).astype(np.float32) / 255.0
mask = np.tile(mask, (4, 1, 1))
mask = mask[None].transpose(0, 1, 2, 3) # what does this step do?
mask = 1 - mask # repaint white, keep black
mask = torch.from_numpy(mask)
return mask
def preprocess(image):
w, h = image.size
w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
image = image.resize((w, h), resample=PIL.Image.LANCZOS)
image = np.array(image).astype(np.float32) / 255.0
image = image[None].transpose(0, 3, 1, 2)
image = torch.from_numpy(image)
return 2.0 * image - 1.0
model_id = "runwayml/stable-diffusion-v1-5"
# 1. 加載autoencoder
vae = AutoencoderKL.from_pretrained(model_id, subfolder="vae")
# 2. 加載tokenizer和text encoder
tokenizer = CLIPTokenizer.from_pretrained(model_id, subfolder="tokenizer")
text_encoder = CLIPTextModel.from_pretrained(model_id, subfolder="text_encoder")
# 3. 加載擴(kuò)散模型UNet
unet = UNet2DConditionModel.from_pretrained(model_id, subfolder="unet")
# 4. 定義noise scheduler
noise_scheduler = DDIMScheduler(
num_train_timesteps=1000,
beta_start=0.00085,
beta_end=0.012,
beta_schedule="scaled_linear",
clip_sample=False, # don't clip sample, the x0 in stable diffusion not in range [-1, 1]
set_alpha_to_one=False,
)
# 將模型復(fù)制到GPU上
device = "cuda"
vae.to(device, dtype=torch.float16)
text_encoder.to(device, dtype=torch.float16)
unet = unet.to(device, dtype=torch.float16)
prompt = "a mecha robot sitting on a bench"
strength = 0.75
guidance_scale = 7.5
batch_size = 1
num_inference_steps = 50
negative_prompt = ""
generator = torch.Generator(device).manual_seed(0)
with torch.no_grad():
# 獲取prompt的text_embeddings
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(device))[0]
# 獲取unconditional text embeddings
max_length = text_input.input_ids.shape[-1]
uncond_input = tokenizer(
[negative_prompt] * batch_size, padding="max_length", max_length=max_length, return_tensors="pt"
)
uncond_embeddings = text_encoder(uncond_input.input_ids.to(device))[0]
# 拼接batch
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
# 設(shè)置采樣步數(shù)
noise_scheduler.set_timesteps(num_inference_steps, device=device)
# 根據(jù)strength計(jì)算timesteps
init_timestep = min(int(num_inference_steps * strength), num_inference_steps)
t_start = max(num_inference_steps - init_timestep, 0)
timesteps = noise_scheduler.timesteps[t_start:]
# 預(yù)處理init_image
init_input = preprocess(input_image)
init_latents = vae.encode(init_input.to(device, dtype=torch.float16)).latent_dist.sample(generator)
init_latents = 0.18215 * init_latents
init_latents = torch.cat([init_latents] * batch_size, dim=0)
init_latents_orig = init_latents
# 處理mask
mask_image = preprocess_mask(input_mask)
mask_image = mask_image.to(device=device, dtype=init_latents.dtype)
mask = torch.cat([mask_image] * batch_size)
# 給init_latents加噪音
noise = torch.randn(init_latents.shape, generator=generator, device=device, dtype=init_latents.dtype)
init_latents = noise_scheduler.add_noise(init_latents, noise, timesteps[:1])
latents = init_latents # 作為初始latents
# Do denoise steps
for t in tqdm(timesteps):
# 這里latens擴(kuò)展2份,是為了同時(shí)計(jì)算unconditional prediction
latent_model_input = torch.cat([latents] * 2)
latent_model_input = noise_scheduler.scale_model_input(latent_model_input, t) # for DDIM, do nothing
# 預(yù)測(cè)噪音
noise_pred = unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample
# CFG
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# 計(jì)算上一步的noisy latents:x_t -> x_t-1
latents = noise_scheduler.step(noise_pred, t, latents).prev_sample
# 將unmask區(qū)域替換原始圖像的nosiy latents
init_latents_proper = noise_scheduler.add_noise(init_latents_orig, noise, torch.tensor([t]))
latents = (init_latents_proper * mask) + (latents * (1 - mask))
# 注意要對(duì)latents進(jìn)行scale
latents = 1 / 0.18215 * latents
image = vae.decode(latents).sample
另外,runwayml在發(fā)布SD 1.5版本的同時(shí)還發(fā)布了一個(gè)inpainting模型:runwayml/stable-diffusion-inpainting,與前面所講不同的是,這是一個(gè)在SD 1.2上finetune的模型。原來(lái)Stable Diffusion的UNet的輸入是64x64x4,為了實(shí)現(xiàn)inpainting,現(xiàn)在給UNet的第一個(gè)卷積層增加5個(gè)channels,分別為masked圖像的latents(經(jīng)過(guò)autoencoder編碼,64x64x4)和mask圖像(直接下采樣8x,64x64x1),增加的權(quán)重填零初始化。
在diffusers中,可以使用StableDiffusionInpaintPipeline來(lái)調(diào)用這個(gè)模型,具體代碼如下:
import torch
from diffusers import StableDiffusionInpaintPipeline
from PIL import Image
from tqdm.auto import tqdm
import PIL
# Load pipeline
model_id = "runwayml/stable-diffusion-inpainting/"
pipe = StableDiffusionInpaintPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda")
prompt = ["a mecha robot sitting on a bench", "a dog sitting on a bench", "a bench"]
generator = torch.Generator("cuda").manual_seed(2023)
input_image = Image.open("overture-creations-5sI6fQgYIuo.png").resize((512, 512))
input_mask = Image.open("overture-creations-5sI6fQgYIuo_mask.png").resize((512, 512))
images = pipe(
prompt=prompt,
image=input_image,
mask_image=input_mask,
num_inference_steps=50,
generator=generator,
).images
其生成的效果圖如下所示:
經(jīng)過(guò)finetune的inpainting在生成細(xì)節(jié)上可能會(huì)更好,但是有可能會(huì)喪失部分文生圖的能力,而且也比較難遷移其它finetune的Stable Diffusion模型。
1.6 模型版本
(1)Stable Diffusion V2
Stability AI公司在2022年11月(stable-diffusion-v2-release)放出了SD 2.0版本,SD 2.0相比SD 1.x版本的主要變動(dòng)在于模型結(jié)構(gòu)和訓(xùn)練數(shù)據(jù)兩個(gè)部分。
首先是模型結(jié)構(gòu),SD 1.x版本的text encoder采用的是OpenAI的CLIP ViT-L/14模型,其模型參數(shù)量為123.65M;而SD 2.0采用了更大的text encoder:基于OpenCLIP在laion-2b數(shù)據(jù)集上訓(xùn)練的CLIP ViT-H/14模型,其參數(shù)量為354.03M,相比原來(lái)的text encoder模型大了約3倍。
然后是訓(xùn)練數(shù)據(jù),前面說(shuō)過(guò)SD 1.x版本其實(shí)最后主要采用Laion-2B中美學(xué)評(píng)分為5以上的子集來(lái)訓(xùn)練,而SD 2.0版本采用評(píng)分在4.5以上的子集,相當(dāng)于擴(kuò)大了訓(xùn)練數(shù)據(jù)集,具體的訓(xùn)練細(xì)節(jié)見(jiàn)model card。 另外SD 2.0除了512x512版本的模型,還包括768x768版本的模型(https://huggingface.co/stabilityai/stable-diffusion-2),所謂的768x768模型是在512x512模型基礎(chǔ)上用圖像分辨率大于768x768的子集繼續(xù)訓(xùn)練的,不過(guò)優(yōu)化目標(biāo)不再是noise_prediction,而是采用Progressive Distillation for Fast Sampling of Diffusion Models論文中所提出的 v-objective。
Stability AI在發(fā)布SD 2.0的同時(shí),還發(fā)布了另外3個(gè)模型:stable-diffusion-x4-upscaler,stable-diffusion-2-inpainting和stable-diffusion-2-depth。
在diffusers庫(kù)中,可以如下使用這個(gè)超分模型(這里的noise level是指推理時(shí)對(duì)低分辨率圖像加入噪音的程度):
import requests
from PIL import Image
from io import BytesIO
from diffusers import StableDiffusionUpscalePipeline
import torch
# load model and scheduler
model_id = "stabilityai/stable-diffusion-x4-upscaler"
pipeline = StableDiffusionUpscalePipeline.from_pretrained(model_id, torch_dtype=torch.float16)
pipeline = pipeline.to("cuda")
# let's download an image
url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-upscale/low_res_cat.png"
response = requests.get(url)
low_res_img = Image.open(BytesIO(response.content)).convert("RGB")
low_res_img = low_res_img.resize((128, 128))
prompt = "a white cat"
upscaled_image = pipeline(prompt=prompt, image=low_res_img, noise_level=20).images[0]
upscaled_image.save("upsampled_cat.png")
stable-diffusion-2-depth是也是在SD 2.0的512x512版本上finetune的模型,它是額外增加了圖像的深度圖作為condition,這里是直接將深度圖下采樣8x,然后和nosiy latent拼接在一起送入U(xiǎn)Net模型中。深度圖可以作為一種結(jié)構(gòu)控制,下圖展示了加入深度圖后生成的圖像效果:
原圖如下:
可以調(diào)用diffusers庫(kù)中的StableDiffusionDepth2ImgPipeline
來(lái)實(shí)現(xiàn)基于深度圖控制的文生圖:
import torch
import requests
from PIL import Image
from diffusers import StableDiffusionDepth2ImgPipeline
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained(
"stabilityai/stable-diffusion-2-depth",
torch_dtype=torch.float16,
).to("cuda")
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
init_image = Image.open(requests.get(url, stream=True).raw)
prompt = "two tigers"
n_propmt = "bad, deformed, ugly, bad anotomy"
image = pipe(prompt=prompt, image=init_image, negative_prompt=n_propmt, strength=0.7).images[0]
除此之外,Stability AI公司還開(kāi)源了兩個(gè)加強(qiáng)版的autoencoder:ft-EMA
和ft-MSE
(前者使用L1 loss后者使用MSE loss),它們是在LAION數(shù)據(jù)集繼續(xù)finetune decoder來(lái)增強(qiáng)重建效果。
(2)Stable Diffusion V2.1
在SD 2.0版本發(fā)布幾周后,Stability AI又發(fā)布了SD 2.1。SD 2.0在訓(xùn)練過(guò)程中采用NSFW檢測(cè)器過(guò)濾掉了可能包含色情的圖像(punsafe=0.1),但是也同時(shí)過(guò)濾了很多人像圖片,這導(dǎo)致SD 2.0在人像生成上效果可能較差,所以SD 2.1是在SD 2.0的基礎(chǔ)上放開(kāi)了限制(punsafe=0.98)繼續(xù)finetune,所以增強(qiáng)了人像的生成效果。
(3)Stable Diffusion unclip
Stability AI在2023年3月份,又放出了基于Stable Diffusion的另外一個(gè)模型:stable-diffusion-reimagine,它可以實(shí)現(xiàn)單個(gè)圖像的變換,即image variations,目前該模型已經(jīng)在在huggingface上開(kāi)源:stable-diffusion-2-1-unclip。
這個(gè)模型是借鑒了OpenAI的DALLE2(又稱(chēng)unCLIP),unCLIP是基于CLIP的image encoder提取的image embeddings作為condition來(lái)實(shí)現(xiàn)圖像的生成。
Stable Diffusion unCLIP是在原來(lái)的Stable Diffusion模型的基礎(chǔ)上增加了CLIP的image encoder的nosiy image embeddings作為condition。具體來(lái)說(shuō),它在訓(xùn)練過(guò)程中是對(duì)提取的image embeddings施加一定的高斯噪音(也是通過(guò)擴(kuò)散過(guò)程),然后將noise level對(duì)應(yīng)的time embeddings和image embeddings拼接在一起,最后再以class labels的方式送入U(xiǎn)Net。
在diffusers中,可以調(diào)用StableUnCLIPImg2ImgPipeline
來(lái)實(shí)現(xiàn)圖像的變換:
import requests
import torch
from PIL import Image
from io import BytesIO
from diffusers import StableUnCLIPImg2ImgPipeline
#Start the StableUnCLIP Image variations pipeline
pipe = StableUnCLIPImg2ImgPipeline.from_pretrained(
"stabilityai/stable-diffusion-2-1-unclip", torch_dtype=torch.float16, variation="fp16"
)
pipe = pipe.to("cuda")
#Get image from URL
url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/stable_unclip/tarsila_do_amaral.png"
response = requests.get(url)
init_image = Image.open(BytesIO(response.content)).convert("RGB")
#Pipe to make the variation
images = pipe(init_image).images
images[0].save("tarsila_variation.png")
其實(shí)在Stable Diffusion unCLIP之前,已經(jīng)有Lambda Labs開(kāi)源的sd-image-variations-diffusers,它是在SD 1.4的基礎(chǔ)上finetune的模型,不過(guò)實(shí)現(xiàn)方式是直接將text embeddings替換為image embeddings,這樣也同樣可以實(shí)現(xiàn)圖像的變換。
這里Stable Diffusion unCLIP有兩個(gè)版本:sd21-unclip-l
和sd21-unclip-h
,兩者分別是采用OpenAI CLIP-L
和OpenCLIP-H
模型的image embeddings作為condition。如果要實(shí)現(xiàn)文生圖,還需要像DALLE2那樣訓(xùn)練一個(gè)prior模型,它可以實(shí)現(xiàn)基于文本來(lái)預(yù)測(cè)對(duì)應(yīng)的image embeddings,將prior模型和SD unCLIP接在一起就可以實(shí)現(xiàn)文生圖了。
1.7 其他類(lèi)型的條件生成模型
-
Img2Img
Img2Img是圖片到圖片的轉(zhuǎn)換,包括多種類(lèi)型,如風(fēng)格轉(zhuǎn)換(從照片風(fēng)格轉(zhuǎn)換為動(dòng)漫風(fēng)格)和圖片超分辨率(給定一張低分辨率圖片作為條件,讓模型生成對(duì)應(yīng)的高分辨率圖片,類(lèi)似于Stable Diffusion Upscaler)。 -
Inpainting
Inpainting又稱(chēng)為圖片修復(fù),它是圖片的部分掩膜到圖片的轉(zhuǎn)換,模型會(huì)根據(jù)掩膜的區(qū)域信息和掩膜之外的全局結(jié)構(gòu)信息,用掩膜區(qū)域生成連貫的圖片,而掩膜區(qū)域之外則與原圖保持一致。 -
Depth2Img
Depth2Img采用圖片的深度圖作為條件,模型會(huì)生成與深度圖本身相似的具有全局結(jié)構(gòu)的圖片。
1.8 使用DreamBooth進(jìn)行微調(diào)
DreamBooth是一種個(gè)性化訓(xùn)練一個(gè)文本到圖像模型的方法,只需要提供一個(gè)主題的3~5張圖像,就能教會(huì)模型有關(guān)這個(gè)主題的各種概念,從而在不同的場(chǎng)景和視圖中生成這個(gè)主題的相關(guān)圖像。
2. 實(shí)戰(zhàn)Stable Diffusion
2.1 環(huán)境準(zhǔn)備
!pip install -Uq diffusers fifty accelerate transformers
查看GPU:
import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using device: {device}')
下載圖像:
import torch
import requests
from PIL import Image
from io import BytesIO
from matplotlib import pyplot as plt
from diffusers import (
StableDiffusionPipeline,
StableDiffusionImg2ImgPipeline,
StableDiffusionInpaintPipeline,
StableDiffusionDepth2ImgPipeline
)
def download_image(url):
response = requests.get(url)
return Image.open(BytesIO(response.content)).convert("RGB")
img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png"
mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png"
init_image = download_image(img_url).resize((512, 512))
mask_image = download_image(mask_url).resize((512, 512))
device = "cuda" if torch.cuda.is_available() else "cpu"
2.2 從文本生成圖像
載入管線:
如果GPU顯存不足,可以嘗試通過(guò)如下方法減少對(duì)GPU顯存的使用:
- 載入FP16精度版本(并非所有系統(tǒng)都支持),此時(shí)需要保證所有張量都是torch.float16精度:
pipe = StableDiffusionPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device)
- 開(kāi)啟注意力切分功能,旨在通過(guò)降低速度來(lái)減少GPU顯存的使用,代碼如下:
pipe.enable_attention_slicing()
- 減少所生成圖片的尺寸。
管線加載完畢后,通過(guò)如下代碼,利用文本提示語(yǔ)生成圖片:
# 給生成器設(shè)置一個(gè)隨機(jī)種子
generator = torch.Generator(device=device).manual_seed(42)
pipe_output = pipe(
prompt="Palette knife painting of an autumn cityscape", # 提示語(yǔ):哪里要生成
negative_prompt="Oversaturated, blurry, low quality", # 提示語(yǔ):哪里不需要生成
height=480, width=640, # 圖片大小
guidance_scale=8, # 提示文字的影響程度
num_inference_steps=35, # 推理步數(shù)
generator=generator # 設(shè)置隨機(jī)種子生成器
)
pipe_output.images[0]
主要的調(diào)節(jié)參數(shù)如下:
- width和height用于指定所生成圖片的尺寸,注意它們必須是能被8整除的數(shù)字,只有這樣,VAE才能正常工作。
- 步數(shù)num_inference_steps也會(huì)影響所生成圖片的質(zhì)量,采用默認(rèn)設(shè)置50即可,也可以嘗試將其設(shè)置為20并觀察結(jié)果。
- negative_prompt用于強(qiáng)調(diào)不希望生成的內(nèi)容,這個(gè)參數(shù)一般在無(wú)分類(lèi)器引導(dǎo)的情況下使用。這種添加額外控制的方式特別有效:列出一些不想要的特征,有助于生成更好的結(jié)果。
- guidance_scale決定了無(wú)分類(lèi)器引導(dǎo)的影響強(qiáng)度。增大這個(gè)參數(shù)可以使生成的內(nèi)容更接近給出的文本提示語(yǔ);但如果該參數(shù)過(guò)大,則可能導(dǎo)致結(jié)果過(guò)于飽和,不美觀。
以下代碼能加大guidance_scale參數(shù)的作用:
# 對(duì)比不同的guidance_scale效果(該參數(shù)決定了無(wú)分類(lèi)器引導(dǎo)的影響強(qiáng)度)
cfg_scales = [1.1, 8, 12]
prompt = "A collie with a pink hat"
fig, axs = plt.subplots(1, len(cfg_scales), figsize=(16, 5))
for i, ax in enumerate(axs):
im = pipe(prompt, height=480, width=480,
guidance_scale=cfg_scales[i], num_inference_steps=35,
generator=torch.Generator(device=device).manual_seed(42)).images[0]
ax.imshow(im); ax.set_title(f'CFG Scale {cfg_scales[i]}');
2.3 Stable Diffusion Pipeline
查看Stable Diffusion Pipeline的組成部分:
2.3.1
可變分自編碼器(VAE)是一種模型,VAE可以對(duì)輸入圖像進(jìn)行編碼,得到“壓縮過(guò)”的信息,之后再解碼“隱式的”壓縮信息,得到接近輸入的輸出。
UNet的輸入不是完整的圖片,而是縮小版的特征,這樣可以極大地減少對(duì)計(jì)算資源的使用。
# 創(chuàng)建區(qū)間為(-1, 1)的偽數(shù)據(jù)
images = torch.rand(1, 3, 512, 512).to(device) * 2 - 1
print("Input images shape:", images.shape)
# 編碼到隱空間
with torch.no_grad():
latents = 0.18215 * pipe.vae.encode(images).latent_dist.mean
print("Encoded latents shape:", latents.shape)
# 解碼
with torch.no_grad():
decoded_images = pipe.vae.decode(latents / 0.18215).sample
print("Decoded images shape:", decoded_images.shape)
代碼輸出內(nèi)容:
Input images shape: torch.Size([1, 3, 512, 512])
Encoded latents shape: torch.Size([1, 4, 64, 64])
Decoded images shape: torch.Size([1, 3, 512, 512])
2.3.2 分詞器和文本編碼器
文本編碼器的作用是將輸入的字符串(文本提示語(yǔ))轉(zhuǎn)換成數(shù)值表示形式,這樣才能將其輸入U(xiǎn)Net作為條件。文本則被管線中的分詞器(tokenizer)轉(zhuǎn)換成一系列的token(分詞)。文本編碼器是一個(gè)Transformer模型,它被訓(xùn)練用于CLIP。
下面首先手動(dòng)分詞,并將分詞結(jié)果輸入文本編碼器,然后使用管線的_encode_prompt方法調(diào)用這個(gè)編碼過(guò)程,其間補(bǔ)全或截?cái)喾衷~串的長(zhǎng)度;最后使得分詞串的長(zhǎng)度等于最大長(zhǎng)度7。
接下來(lái),獲取最終的文本特征:
文本嵌入(text embedding)是指文本編碼器中最后一個(gè)Transformer模塊的“隱狀態(tài)”(hidden state),它們將作為UNet中的forward函數(shù)的一個(gè)額外輸入。
2.3.3 UNet
在擴(kuò)散模型中,UNet的作用是接收“帶噪”的輸入并預(yù)測(cè)噪聲,以實(shí)現(xiàn)“去噪”。此處輸入模型的并非圖片,而是圖片的隱式表示形式,此外,除了將用于暗示“帶噪”程度的時(shí)間步作為條件輸入U(xiǎn)Net之外,模型還將文本提示語(yǔ)和文本嵌入也作為UNet的輸入。
首先使用偽輸入嘗試讓模型進(jìn)行預(yù)測(cè)。注意在此過(guò)程中各個(gè)輸入輸出的形狀和大小:
# 創(chuàng)建偽輸入
timestep = pipe.scheduler.timesteps[0]
latents = torch.randn(1, 4, 64, 64).to(device)
text_embeddings = torch.randn(1, 77, 1024).to(device)
# 模型預(yù)測(cè)
with torch.no_grad():
unet_output = pipe.unet(latents, timestep, text_embeddings).sample
print('UNet output shape:', unet_output.shape) # UNet output shape: torch.Size([1, 4, 64, 64])
2.3.4 調(diào)度器
調(diào)度器保存了關(guān)于如何添加噪聲的信息,并管理如何基于模型的預(yù)測(cè)更新“帶噪”樣本。默認(rèn)的調(diào)度器是PNDMScheduler。
可以通過(guò)繪制圖片來(lái)觀察在添加噪聲的過(guò)程中噪聲水平(基于參數(shù) α ˉ \bar{\alpha} αˉ)隨時(shí)間步增加的變化趨勢(shì):
plt.plot(pipe.scheduler.alphas_cumprod, label=r'$\bar{\alpha}$')
plt.xlabel('Timestep (high noise to low noise ->)')
plt.title('Noise schedule')
plt.legend();
嘗試不同的調(diào)度器:
生成的圖片:
2.3.5 DIY采樣循環(huán)
將管線的不同組成部分組裝在一起,復(fù)現(xiàn)整個(gè)管線的功能:
guidance_scale = 8
num_inference_steps=30
prompt = "Beautiful picture of a wave breaking"
negative_prompt = "zoomed in, blurry, oversaturated, warped"
# 對(duì)提示文字進(jìn)行編碼
text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
# 創(chuàng)建隨機(jī)噪聲作為起點(diǎn)
latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
latents *= pipe.scheduler.init_noise_sigma
# 設(shè)置調(diào)度器
pipe.scheduler.set_timesteps(num_inference_steps, device=device)
# 循環(huán)采樣
for i, t in enumerate(pipe.scheduler.timesteps):
latent_model_input = torch.cat([latents] * 2)
latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
with torch.no_grad():
noise_pred = pipe.unet(latent_model_input, t, text_embeddings).sample
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 = pipe.scheduler.step(noise_pred, t, latents).prev_sample
# 將隱變量映射到圖片
with torch.no_grad():
image = pipe.decode_latents(latents.detach())
pipe.numpy_to_pil(image)[0]
生成的結(jié)果:
2.4 其他管線應(yīng)用
2.4.1 Img2Img
Img2Img首先會(huì)對(duì)一張已有的圖片進(jìn)行編碼,得到隱變量后添加隨機(jī)噪聲,并以此作為起點(diǎn)。噪聲的數(shù)量和“去噪”所需步數(shù)決定了Img2Img過(guò)程的“強(qiáng)度”.
Img2Img管線不需要任何特殊的模型,而只需要與文字到圖像模型相同的模型ID,無(wú)需下載新文件。
首先載入Img2Img管線:
model_id = "stabilityai/stable-diffusion-2-1-base"
img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device)
Img2Img管線代碼:
result_image = img2img_pipe(
prompt="An oil painting of a man on a bench",
image = init_image,
strength = 0.6, # 強(qiáng)度:0表示完全不起作用,1表示作用強(qiáng)度最大
).images[0]
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(result_image);axs[1].set_title('Result');
2.4.2 Inpainting
關(guān)于Inpainting的內(nèi)容上文已經(jīng)詳細(xì)說(shuō)明了,這里回顧以下。Stable Diffusion模型接收一張掩模圖片作為額外條件輸入,該掩模圖片與輸入圖片的尺寸一致,白色區(qū)域表示要替換的部分,黑色區(qū)域表示要保留的部分。
以下代碼展示了如何載入StableDiffusionInpaintPipelineLegacy管線并將其應(yīng)用于前面輸入的示例圖片和掩膜圖片:
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting", revision="fp16", torch_dtype=torch.float16)
pipe = pipe.to(device)
prompt = "A small robot, high resolution, sitting on a park bench"
image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0]
fig, axs = plt.subplots(1, 3, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(mask_image);axs[1].set_title('Mask')
axs[2].imshow(image);axs[2].set_title('Result');
Hugging Face Spaces上的一個(gè)示例Space應(yīng)用就使用了一個(gè)名為CLIPSeg
的模型,旨在根據(jù)文字描述自動(dòng)地通過(guò)掩膜去掉一個(gè)物體。
2.4.3 Depth2Image
Depth2Image采用深度預(yù)測(cè)模型來(lái)預(yù)測(cè)一個(gè)深度圖,該深度圖被輸入微調(diào)過(guò)的UNet以生成圖片。希望生成的圖片既能保留原始圖片的深度信息和總體結(jié)構(gòu),同時(shí)又能在相關(guān)部分填入全新的內(nèi)容:
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth",revision="fp16", torch_dtype=torch.float16)
pipe = pipe.to(device)
prompt = "An oil painting of a man on a bench"
image = pipe(prompt=prompt, image=init_image).images[0]
fig, axs = plt.subplots(1, 2, figsize=(16, 5))
axs[0].imshow(init_image);axs[0].set_title('Input Image')
axs[1].imshow(image);axs[1].set_title('Result');
Depth2Img管線的生成結(jié)果:
3. Stable Diffusion的特色應(yīng)用
3.1 個(gè)性化生成
個(gè)性化生成是指的生成特定的角色或者風(fēng)格,比如給定自己幾張肖像來(lái)利用SD來(lái)生成個(gè)性化頭像。在個(gè)性化生成方面,比較重要的兩個(gè)工作是英偉達(dá)的Textual Inversion
和谷歌的DreamBooth
。
-
Textual Inversion
這個(gè)工作的核心思路是基于用戶(hù)提供的3~5張?zhí)囟ǜ拍睿ㄎ矬w或者風(fēng)格)的圖像來(lái)學(xué)習(xí)一個(gè)特定的text embeddings,實(shí)際上只用一個(gè)word embedding就足夠了。Textual Inversion
不需要finetune UNet,而且由于text embeddings較小,存儲(chǔ)成本很低。目前diffusers庫(kù)已經(jīng)支持textual_inversion的訓(xùn)練。 -
DreamBooth
原本是谷歌提出的應(yīng)用在Imagen上的個(gè)性化生成,但是它實(shí)際上也可以擴(kuò)展到Stable Diffusion上。DreamBooth首先為特定的概念尋找一個(gè)特定的描述詞[V],這個(gè)特定的描述詞只要是稀有的就可以,然后與Textual Inversion不同的是DreamBooth需要finetune UNet,這里為了防止過(guò)擬合,增加了一個(gè)class-specific prior preservation loss(基于SD生成同class圖像加入batch里面訓(xùn)練)來(lái)進(jìn)行正則化。
由于finetune了UNet,DreamBooth往往比Textual Inversion要表現(xiàn)的要好,但是DreamBooth的存儲(chǔ)成本較高。目前diffusers庫(kù)已經(jīng)支持dreambooth訓(xùn)練,也可以在sd-dreambooth-library中找到其他人上傳的模型。
還有很多其它的研究工作,比如Adobe提出的Custom Diffusion,相比DreamBooth,它只finetune了UNet的attention模塊的KV權(quán)重矩陣,同時(shí)優(yōu)化一個(gè)新概念的token。
3.2 風(fēng)格化finetune模型
Stable Diffusion的另外一大應(yīng)用是采用特定風(fēng)格的數(shù)據(jù)集進(jìn)行finetune,這使得模型“過(guò)擬合”在特定的風(fēng)格上。之前比較火的novelai就是基于二次元數(shù)據(jù)在Stable Diffusion上finetune的模型,雖然它失去了生成其它風(fēng)格圖像的能力,但是它在二次元圖像的生成效果上比原來(lái)的Stable Diffusion要好很多。
目前已經(jīng)有很多風(fēng)格化的模型在huggingface上開(kāi)源,這里也列出一些:
- andite/anything-v4.0:二次元或者動(dòng)漫風(fēng)格圖像
- dreamlike-art/dreamlike-diffusion-1.0:藝術(shù)風(fēng)格圖像
- prompthero/openjourney:mdjrny-v4風(fēng)格圖像
值得說(shuō)明的一點(diǎn)是,目前finetune Stable Diffusion模型的方法主要有兩種:一種是直接finetune了UNet,但是容易過(guò)擬合,而且存儲(chǔ)成本;另外一種低成本的方法是基于微軟的LoRA,LoRA本來(lái)是用于finetune語(yǔ)言模型的,但是現(xiàn)在已經(jīng)可以用來(lái)finetune Stable Diffusion模型了
3.3 圖像編輯
圖像編輯也是Stable Diffusion比較火的應(yīng)用方向,這里所說(shuō)的圖像編輯是指的是使用Stable Diffusion來(lái)實(shí)現(xiàn)對(duì)圖片的局部編輯。這里列舉兩個(gè)比較好的工作:谷歌的prompt-to-prompt
和加州伯克利的instruct-pix2pix
。
- 谷歌的prompt-to-prompt的核心是基于UNet的cross attention maps來(lái)實(shí)現(xiàn)對(duì)圖像的編輯,它的好處是不需要finetune模型,但是主要用在編輯用Stable Diffusion生成的圖像。
- 伯克利的instruct-pix2pix這個(gè)工作基于GPT-3和prompt-to-prompt構(gòu)建了pair的數(shù)據(jù)集,然后在Stable Diffusion上進(jìn)行finetune,它可以輸入text instruct對(duì)圖像進(jìn)行編輯
3.4 可控生成
可控生成是Stable Diffusion最近比較火的應(yīng)用,這主要?dú)w功于ControlNet,基于ControlNet可以實(shí)現(xiàn)對(duì)很多種類(lèi)的可控生成,比如邊緣,人體關(guān)鍵點(diǎn),草圖和深度圖等等。
其實(shí)在ControlNet之前,也有一些可控生成的工作,比如stable-diffusion-2-depth也屬于可控生成,但是沒(méi)有太火。
與ControlNet同期的工作還有騰訊的T2I-Adapter以及阿里的composer-page:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-814606.html
3.5 Stable-Diffusion-WebUI
最后要介紹的一個(gè)比較火的應(yīng)用stable-diffusion-webui其實(shí)是用來(lái)支持Stable Diffusion出圖的一個(gè)web工具,它算是基于gradio
框架實(shí)現(xiàn)了Stable Diffusion的快速部署,不僅支持Stable Diffusion的最基礎(chǔ)的文生圖、圖生圖以及圖像inpainting功能,還支持Stable Diffusion的其它拓展功能,很多基于Stable Diffusion的拓展應(yīng)用可以用插件的方式安裝在webui上。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-814606.html
參考資料
- stability.ai
- dreamstudio.ai
- 硬核解讀Stable Diffusion(系列一)
- OpenAI/CLIP
- CLIP: Connecting text and images
- 十分鐘讀懂Stable Diffusion運(yùn)行原理
- 硬核解讀Stable Diffusion(系列二)
- 硬核解讀Stable Diffusion(系列三)
到了這里,關(guān)于【擴(kuò)散模型】萬(wàn)字長(zhǎng)文全面理解與應(yīng)用Stable Diffusion的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!