简介
这个笔记本将介绍如何使用 Stable Diffusion 来创建和修改图像的基本知识,使用现有的管道。我们还将简要介绍管道中的关键组件,而进一步探索这些组件将留给深入研究的笔记本。具体来说,我们将介绍
- 使用
StableDiffusionPipeline
从文本生成图像,并尝试使用可用参数 - 查看管道中的某些关键组件的运行情况
- 将此模型称为“潜在扩散模型”的 VAE
- 处理文本提示的标记器和文本编码器
- UNet 本身
- 调度器,以及探索不同的调度器
- 使用管道组件复制采样循环
- 使用 Img2Img 管道编辑现有图像
- 使用 Inpainting 和 Depth2Img 管道
❓如果您有任何问题,请在 Hugging Face Discord 服务器上的 #diffusion-models-class
频道上发布它们。如果您尚未注册,您可以在此处注册:https://huggingface.co/join/discord
设置
%pip install -Uq diffusers ftfy accelerate
# Installing transformers from source for now since we need the latest version for Depth2Img
%pip install -Uq git+https://github.com/huggingface/transformers
import torch
import requests
from PIL import Image
from io import BytesIO
from matplotlib import pyplot as plt
# We'll be exploring a number of pipelines today!
from diffusers import (
StableDiffusionPipeline,
StableDiffusionImg2ImgPipeline,
StableDiffusionInpaintPipeline,
StableDiffusionDepth2ImgPipeline,
)
# We'll use a couple of demo images later in the notebook
def download_image(url):
response = requests.get(url)
return Image.open(BytesIO(response.content)).convert("RGB")
# Download images for inpainting example
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))
# Set device
device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu"
从文本生成图像
让我们加载一个 Stable Diffusion 管道,看看它能做什么。Stable Diffusion 有多个不同的版本,在撰写本文时最新版本为 2.1 版。如果您想探索旧版本,只需将模型 ID 替换为相应的模型(例如,您可以尝试“CompVis/stable-diffusion-v1-4”或从 dreambooth 概念库 中选择一个模型)。
# Load the pipeline
model_id = "stabilityai/stable-diffusion-2-1-base"
pipe = StableDiffusionPipeline.from_pretrained(model_id).to(device)
如果您运行时 GPU 内存不足,可以采取一些措施来减少 RAM 使用量
加载 FP16 版本(并非所有系统都支持)。使用它,您可能还需要在尝试使用管道的各个组件时将张量转换为 torch.float16
pipe = StableDiffusionPipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16).to(device)
启用注意力切片。这会减少 GPU 内存使用量,但会稍微降低速度
pipe.enable_attention_slicing()
减少您要生成的图像的大小
管道加载完成后,我们可以使用以下代码根据提示生成图像
>>> # Set up a generator for reproducibility
>>> generator = torch.Generator(device=device).manual_seed(42)
>>> # Run the pipeline, showing some of the available arguments
>>> pipe_output = pipe(
... prompt="Palette knife painting of an autumn cityscape", # What to generate
... negative_prompt="Oversaturated, blurry, low quality", # What NOT to generate
... height=480,
... width=640, # Specify the image size
... guidance_scale=8, # How strongly to follow the prompt
... num_inference_steps=35, # How many steps to take
... generator=generator, # Fixed random seed
... )
>>> # View the resulting image
>>> pipe_output.images[0]
练习:花一些时间玩上面这个单元格,使用您自己的提示并调整设置,看看它们如何影响输出。使用不同的随机种子或删除 generator
参数以每次获得不同的结果。
关键参数进行调整
- 宽度和高度指定生成的图像的大小。它们必须能被 8 整除,才能使 VAE 正常工作(我们将在后面的部分看到)。
- 步数会影响生成的质量。默认值(50)效果很好,但在某些情况下,您只需使用 20 步就能达到目的,这对实验非常有用。
- 负面提示在无分类器引导过程中使用,它可以成为一种有用的方式来添加额外的控制。您可以省略它,但许多用户发现将一些不希望看到的描述列在负面提示中很有用,如上所示。
guidance_scale
参数决定无分类器引导 (CFG) 的强度。较高的比例会使生成的图像更匹配提示,但如果比例过高,结果可能会过度饱和且令人不快。
如果您正在寻找提示灵感,Stable Diffusion 提示手册 是一个不错的起点。
您可以在以下单元格中看到增加引导比例的效果
>>> # @markdown comparing guidance scales:
>>> cfg_scales = [1.1, 8, 12] # @param
>>> prompt = "A collie with a pink hat" # @param
>>> 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]}")
调整上面的值以尝试不同的比例和提示。当然,解释是主观的,但就我个人而言,8-12 范围内的任何值都比该范围之下或之上的值产生的结果更好。
管道组件
我们正在使用的 StableDiffusionPipeline
比我们在前面几个单元中探索过的 DDPMPipeline
更复杂。除了 UNet 和调度器之外,管道中还包含许多其他组件
>>> print(list(pipe.components.keys())) # List components
['vae', 'text_encoder', 'tokenizer', 'unet', 'scheduler', 'safety_checker', 'feature_extractor']
为了更好地理解管道的工作原理,让我们分别简要查看每个组件的运行情况,然后将它们放在一起,自己复制管道的功能。
VAE
变分自编码器 (VAE) 是一种可以将输入编码成压缩表示,然后将这个“潜在”表示解码回接近原始输入的模型。在用 Stable Diffusion 生成图像时,我们首先会**通过在 VAE 的“潜在空间”中应用扩散过程来生成潜在表示**,然后在**最后对其进行解码**以查看生成的图像。
以下是一些代码,可以将输入图像编码成潜在表示,然后使用 VAE 再次解码。
>>> # Create some fake data (a random image, range (-1, 1))
>>> images = torch.rand(1, 3, 512, 512).to(device) * 2 - 1
>>> print("Input images shape:", images.shape)
>>> # Encode to latent space
>>> with torch.no_grad():
... latents = 0.18215 * pipe.vae.encode(images).latent_dist.mean
>>> print("Encoded latents shape:", latents.shape)
>>> # Decode again
>>> with torch.no_grad():
... decoded_images = pipe.vae.decode(latents / 0.18215).sample
>>> print("Decoded images shape:", decoded_images.shape)
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])
如您所见,512x512 图像被压缩成一个 64x64 的潜在表示(具有四个通道)。每个空间维度缩小 8 倍,这就是指定宽度和高度需要是 8 的倍数的原因。
处理这些信息丰富的 4x64x64 潜在表示比处理 512 像素的大型图像更有效,可以实现更快的扩散模型,这些模型需要更少的资源来训练和使用。VAE 解码过程并不完美,但它足够好,以至于质量上的小幅牺牲通常是值得的。
注意:以上代码示例包含 0.18215 的缩放因子,该因子与 SD 训练期间使用的处理过程相匹配。
分词器和文本编码器
文本编码器的目标是将输入字符串(提示)转换成可以作为条件传递给 UNet 的数字表示形式。文本首先使用管道中的分词器转换成一系列标记。文本编码器具有约 50,000 个标记的词汇表 - 任何不在此词汇表中的单词都会被拆分成更小的子词。然后将标记输入文本编码器模型本身 - 这是一种最初训练为 CLIP 的文本编码器的 Transformer 模型。希望这种预训练的 Transformer 模型已经学习了丰富的文本表示形式,这些表示形式对扩散任务也有用。
让我们通过对示例提示进行编码来测试这个过程,首先手动进行分词并将它输入文本编码器,然后使用管道中的encode_prompt
方法来显示整个过程,包括将长度填充/截断到 77 个标记的最大长度。
>>> # Tokenizing and encoding an example prompt manually
>>> # Tokenize
>>> input_ids = pipe.tokenizer(["A painting of a flooble"])["input_ids"]
>>> print("Input ID -> decoded token")
>>> for input_id in input_ids[0]:
... print(f"{input_id} -> {pipe.tokenizer.decode(input_id)}")
>>> # Feed through CLIP text encoder
>>> input_ids = torch.tensor(input_ids).to(device)
>>> with torch.no_grad():
... text_embeddings = pipe.text_encoder(input_ids)["last_hidden_state"]
>>> print("Text embeddings shape:", text_embeddings.shape)
Input ID -> decoded token 49406 -> <|startoftext|> 320 -> a 3086 -> painting 539 -> of 320 -> a 4062 -> floo 1059 -> ble 49407 -> <|endoftext|> Text embeddings shape: torch.Size([1, 8, 1024])
# Get the final text embeddings using the pipeline's encode_prompt function
text_embeddings = pipe._encode_prompt("A painting of a flooble", device, 1, False, "")
text_embeddings.shape
这些文本嵌入(即文本编码器模型中最后一个 Transformer 模块的“隐藏状态”)将作为附加参数传递给 UNet 的forward
方法,我们将在下一节中看到。
UNet
UNet 接受一个噪声输入并预测噪声,就像我们在前面单元中看到的 UNet 一样。与前面的例子不同,输入不是图像,而是图像的潜在表示。除了时间步长条件外,这个 UNet 还接收提示的文本嵌入作为额外的输入。它正在对一些虚拟数据进行预测。
>>> # Dummy inputs
>>> timestep = pipe.scheduler.timesteps[0]
>>> latents = torch.randn(1, 4, 64, 64).to(device)
>>> text_embeddings = torch.randn(1, 77, 1024).to(device)
>>> # Model prediction
>>> with torch.no_grad():
... unet_output = pipe.unet(latents, timestep, text_embeddings).sample
>>> print("UNet output shape:", unet_output.shape) # Same shape as the input latents
UNet output shape: torch.Size([1, 4, 64, 64])
调度器
调度器存储噪声调度,并基于模型预测管理更新噪声样本。默认调度器是PNDMScheduler
,但您可以使用其他调度器(例如LMSDiscreteScheduler
),只要它们以相同的配置进行初始化。
我们可以绘制噪声调度以查看噪声水平(基于 $\bar{\alpha}$) 随时间的变化情况。
>>> plt.plot(pipe.scheduler.alphas_cumprod, label=r"$\bar{\alpha}$")
>>> plt.xlabel("Timestep (high noise to low noise ->)")
>>> plt.title("Noise schedule")
>>> plt.legend()
如果您想尝试不同的调度器,可以像下面这样换用一个新的调度器。
>>> from diffusers import LMSDiscreteScheduler
>>> # Replace the scheduler
>>> pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config)
>>> # Print the config
>>> print("Scheduler config:", pipe.scheduler)
>>> # Generate an image with this new scheduler
>>> pipe(
... prompt="Palette knife painting of an winter cityscape",
... height=480,
... width=480,
... generator=torch.Generator(device=device).manual_seed(42),
... ).images[0]
Scheduler config: LMSDiscreteScheduler { "_class_name": "LMSDiscreteScheduler", "_diffusers_version": "0.11.1", "beta_end": 0.012, "beta_schedule": "scaled_linear", "beta_start": 0.00085, "clip_sample": false, "num_train_timesteps": 1000, "prediction_type": "epsilon", "set_alpha_to_one": false, "skip_prk_steps": true, "steps_offset": 1, "trained_betas": null }
您可以在这里阅读有关使用不同调度器的更多信息 here.
DIY 采样循环
现在我们已经看到了所有这些组件的实际运行情况,我们可以将它们组合在一起以复制管道的功能。
>>> guidance_scale = 8 # @param
>>> num_inference_steps = 30 # @param
>>> prompt = "Beautiful picture of a wave breaking" # @param
>>> negative_prompt = "zoomed in, blurry, oversaturated, warped" # @param
>>> # Encode the prompt
>>> text_embeddings = pipe._encode_prompt(prompt, device, 1, True, negative_prompt)
>>> # Create our random starting point
>>> latents = torch.randn((1, 4, 64, 64), device=device, generator=generator)
>>> latents *= pipe.scheduler.init_noise_sigma
>>> # Prepare the scheduler
>>> pipe.scheduler.set_timesteps(num_inference_steps, device=device)
>>> # Loop through the sampling timesteps
>>> for i, t in enumerate(pipe.scheduler.timesteps):
... # Expand the latents if we are doing classifier free guidance
... latent_model_input = torch.cat([latents] * 2)
... # Apply any scaling required by the scheduler
... latent_model_input = pipe.scheduler.scale_model_input(latent_model_input, t)
... # Predict the noise residual with the UNet
... with torch.no_grad():
... noise_pred = pipe.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 = pipe.scheduler.step(noise_pred, t, latents).prev_sample
>>> # Decode the resulting latents into an image
>>> with torch.no_grad():
... image = pipe.decode_latents(latents.detach())
>>> # View
>>> pipe.numpy_to_pil(image)[0]
在大多数情况下,使用现有的管道会更容易,但是拥有这个可定制的采样循环有助于理解和修改每个组件的工作原理。如果您想查看这个代码以及对所有不同组件的深入探索和修改,请查看“Stable Diffusion 深入研究” notebook 和 video,以获得更深入的探索。
额外的管道
那么除了根据提示生成图像外,我们还能做什么?很多!在本节中,我们将演示一些很酷的管道,让您体验 Stable Diffusion 可以用于的其他一些任务。其中一些任务需要下载新的模型,所以如果您时间紧迫,您可以快速浏览本节,只需查看现有的输出,而不必自己下载和运行所有模型。
Img2Img
在前面的示例中,我们通过从随机潜在表示开始并应用完整的扩散采样循环来从头开始生成图像。但是,我们不必从头开始。Img2Img 管道首先将现有图像编码成一组潜在表示,然后向潜在表示中添加一些噪声,并将其作为起点。添加的噪声量和应用的去噪步骤数量决定了 Img2Img 过程的“强度”。添加少量噪声(低强度)将导致变化很小,而添加最大噪声量并运行完整的去噪过程将生成一个几乎不与输入图像相似的图像,除了在整体结构上有一些相似之处。
此管道不需要任何特殊的模型,因此只要模型 ID 与我们上面的文本到图像示例相同,就不需要下载任何新文件。
# Loading an Img2Img pipeline
model_id = "stabilityai/stable-diffusion-2-1-base"
img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id).to(device)
在“设置”部分中,我们加载了一个示例init_image
以用于此演示,但您可以用自己的图像替换它,如果您更喜欢。以下是在实际操作中的管道。
>>> # Apply Img2Img
>>> result_image = img2img_pipe(
... prompt="An oil painting of a man on a bench",
... image=init_image, # The starting image
... strength=0.6, # 0 for no change, 1.0 for max strength
... ).images[0]
>>> # View the result
>>> 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")
练习:尝试使用此管道。尝试使用自己的图像,或使用不同的强度和提示进行尝试。您可以使用与文本到图像管道相同的许多参数,因此可以尝试不同的尺寸、步骤数量等。
Inpainting
如果我们想保留输入图像的一部分不变,但在其他部分生成新的内容呢?这叫做“Inpainting”。虽然可以使用与前面演示相同的模型(通过StableDiffusionInpaintPipelineLegacy
)来实现,但我们可以通过使用 Stable Diffusion 的自定义微调版本来获得更好的效果,该版本将蒙版图像和蒙版本身作为额外的条件。蒙版图像应与输入图像具有相同的形状,其中白色表示要替换的区域,黑色表示要保持不变的区域。以下是如何加载此类管道并将其应用于“设置”部分中加载的示例图像和蒙版。
# Load the inpainting pipeline (requires a suitable inpainting model)
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting")
pipe = pipe.to(device)
>>> # Inpaint with a prompt for what we want the result to look like
>>> prompt = "A small robot, high resolution, sitting on a park bench"
>>> image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0]
>>> # View the result
>>> 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")
当与另一个模型结合时,这将尤其强大,可以自动生成蒙版。例如,这个演示空间 使用一个名为 CLIPSeg 的模型,根据文本描述屏蔽要替换的对象。
旁白:管理您的模型缓存
探索不同的管道和模型变体可能会占用您的磁盘空间。您可以使用以下命令查看当前已下载的模型:
>>> !ls ~/.cache/huggingface/hub/ # List the contents of the cache directory
models--CompVis--stable-diffusion-v1-4 models--ddpm-bedroom-256 models--google--ddpm-bedroom-256 models--google--ddpm-celebahq-256 models--runwayml--stable-diffusion-inpainting models--stabilityai--stable-diffusion-2-1-base
查看 有关缓存的文档,以了解如何有效地查看和管理您的缓存。
Depth2Image
输入图像、深度图像和生成的示例(图像来源:StabilityAI)
Img2Img 很棒,但有时我们想创建一个具有原始图像构图但颜色或纹理完全不同的新图像。找到一个可以保留我们想要的布局但又不保留输入颜色的 Img2Img 强度可能很困难。
是时候使用另一个微调模型了!这个模型在生成时将深度信息作为额外的条件。该管道使用深度估计模型创建深度图,然后在生成图像时将其馈送到微调的 UNet,以(希望)保留初始图像的深度和结构,同时填充完全新的内容。
# Load the Depth2Img pipeline (requires a suitable model)
pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth")
pipe = pipe.to(device)
>>> # Inpaint with a prompt for what we want the result to look like
>>> prompt = "An oil painting of a man on a bench"
>>> image = pipe(prompt=prompt, image=init_image).images[0]
>>> # View the result
>>> 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")
请注意输出与 img2img 示例的比较 - 这里有更多的颜色变化,但整体结构仍然忠实于原始图像。在本例中,这不是理想的,因为该男子被赋予了一些非常奇怪的解剖结构来匹配狗的形状,但在某些情况下,这非常有用。有关此方法的“杀手级应用”的示例,请查看 这条推文,它展示了使用深度模型为 3D 场景纹理化!
下一步
希望这能让你对 Stable Diffusion 的多种功能有所了解!玩腻了这个笔记本中的示例后,可以查看 **DreamBooth 黑客马拉松** 笔记本,了解如何微调您自己的 Stable Diffusion 版本,该版本可与我们在此看到的文本到图像或 img2img 管道一起使用。
如果您想更深入地了解不同组件的工作原理,请查看 **Stable Diffusion 深入分析** 笔记本,其中将详细介绍并展示一些我们可以执行的其他技巧。
一定要与我们和社区分享您的作品!