扩散课程文档

微调和引导

Hugging Face's logo
加入Hugging Face社区

并获得增强文档体验

开始使用

Open In Colab

微调和引导

在本笔记本中,我们将介绍两种主要方法来调整现有的扩散模型。

  • 使用**微调**,我们将对现有模型进行重新训练,使其能够生成新类型的数据。
  • 使用**引导**,我们将利用现有模型并在推理时引导生成过程,以实现更多控制。

学习目标:

在本笔记本结束时,您将了解如何

  • 创建采样循环并使用新的调度器更快地生成样本
  • 在新的数据上对现有的扩散模型进行微调,包括
    • 使用梯度累积来解决小批次的一些问题
    • 在训练期间将样本记录到Weights and Biases以监控进度(通过随附的示例脚本)
    • 保存生成的管道并将其上传到Hub
  • 通过额外的损失函数引导采样过程,以增强对现有模型的控制,包括
    • 使用简单的基于颜色的损失探索不同的引导方法
    • 使用CLIP根据文本提示引导生成
    • 使用Gradio和 🤗 Spaces共享自定义采样循环

❓如果您有任何问题,请在Hugging Face Discord服务器的#diffusion-models-class频道中发布。如果您尚未注册,可以在这里注册:https://huggingface.co/join/discord

设置和导入

要将微调后的模型保存到Hugging Face Hub,您需要使用具有写入权限的令牌登录。以下代码将提示您输入此令牌并链接到您帐户的相关令牌页面。如果您想使用训练脚本在模型训练时记录样本,则还需要一个Weights and Biases帐户 - 同样,代码应在需要时提示您登录。

除此之外,唯一的设置是安装一些依赖项,导入我们需要的所有内容,并指定我们将使用哪个设备。

%pip install -qq diffusers datasets accelerate wandb open-clip-torch
>>> # Code to log in to the Hugging Face Hub, needed for sharing models
>>> # Make sure you use a token with WRITE access
>>> from huggingface_hub import notebook_login

>>> notebook_login()
Token is valid.
Your token has been saved in your configured git credential helpers (store).
Your token has been saved to /root/.huggingface/token
Login successful
import numpy as np
import torch
import torch.nn.functional as F
import torchvision
from datasets import load_dataset
from diffusers import DDIMScheduler, DDPMPipeline
from matplotlib import pyplot as plt
from PIL import Image
from torchvision import transforms
from tqdm.auto import tqdm
device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu"

加载预训练管道

为了开始本笔记本,让我们加载一个现有的管道,看看我们能用它做什么。

image_pipe = DDPMPipeline.from_pretrained("google/ddpm-celebahq-256")
image_pipe.to(device)

生成图像就像运行管道的__call__方法一样简单,只需像调用函数一样调用它即可。

>>> images = image_pipe().images
>>> images[0]

很不错,但很慢!因此,在我们进入今天的主题之前,让我们先看看实际的采样循环,并了解如何使用更高级的采样器来加快速度。

使用DDIM加速采样

在每个步骤中,模型都会接收一个带有噪声的输入,并被要求预测噪声(以及由此推断出的完全去噪图像可能是什么样子)。最初,这些预测并不是很好,这就是我们将过程分解成许多步骤的原因。但是,使用1000多个步骤已被发现是不必要的,最近的研究热潮已经探索了如何在尽可能少的步骤中获得良好的样本。

在 🤗 Diffusers 库中,这些**采样方法由调度器处理**,调度器必须通过step()函数执行每个更新。为了生成图像,我们从随机噪声$x$开始。然后,对于调度器噪声计划中的每个时间步,我们将噪声输入$x$馈送到模型,并将生成的预测传递给step()函数。这将返回一个具有prev_sample属性的输出 - 因为我们正在时间上“向后”从高噪声到低噪声(与正向扩散过程相反)。

让我们看看它是如何工作的!首先,我们加载一个调度器,这里是一个基于论文去噪扩散隐式模型的DDIMScheduler,它可以在比原始DDPM实现少得多的步骤中提供不错的样本。

# Create new scheduler and set num inference steps
scheduler = DDIMScheduler.from_pretrained("google/ddpm-celebahq-256")
scheduler.set_timesteps(num_inference_steps=40)

您可以看到,此模型总共执行40个步骤,每个步骤相当于原始1000步计划的25个步骤。

scheduler.timesteps

让我们创建4个随机图像,并遍历采样循环,在过程中查看当前的$x$和预测的去噪版本。

>>> # The random starting point
>>> x = torch.randn(4, 3, 256, 256).to(device)  # Batch of 4, 3-channel 256 x 256 px images

>>> # Loop through the sampling timesteps
>>> for i, t in tqdm(enumerate(scheduler.timesteps)):

...     # Prepare model input
...     model_input = scheduler.scale_model_input(x, t)

...     # Get the prediction
...     with torch.no_grad():
...         noise_pred = image_pipe.unet(model_input, t)["sample"]

...     # Calculate what the updated sample should look like with the scheduler
...     scheduler_output = scheduler.step(noise_pred, t, x)

...     # Update x
...     x = scheduler_output.prev_sample

...     # Occasionally display both x and the predicted denoised images
...     if i % 10 == 0 or i == len(scheduler.timesteps) - 1:
...         fig, axs = plt.subplots(1, 2, figsize=(12, 5))

...         grid = torchvision.utils.make_grid(x, nrow=4).permute(1, 2, 0)
...         axs[0].imshow(grid.cpu().clip(-1, 1) * 0.5 + 0.5)
...         axs[0].set_title(f"Current x (step {i})")

...         pred_x0 = scheduler_output.pred_original_sample  # Not available for all schedulers
...         grid = torchvision.utils.make_grid(pred_x0, nrow=4).permute(1, 2, 0)
...         axs[1].imshow(grid.cpu().clip(-1, 1) * 0.5 + 0.5)
...         axs[1].set_title(f"Predicted denoised images (step {i})")
...         plt.show()

如您所见,初始预测不是很好,但随着过程的进行,预测输出变得越来越精致。如果您好奇step()函数内部发生了哪些数学运算,请使用以下命令检查(注释良好的)代码:

# ??scheduler.step

您还可以将此新的调度器替换为管道附带的原始调度器,并进行如下采样:

>>> image_pipe.scheduler = scheduler
>>> images = image_pipe(num_inference_steps=40).images
>>> images[0]

好的,我们现在可以在合理的时间内获取样本了!这应该会加快我们完成本笔记本其余部分的速度 :)

微调

现在到了有趣的部分!鉴于这个预训练的管道,我们如何重新训练模型以根据新的训练数据生成图像?

事实证明,这看起来几乎与从头开始训练模型相同(正如我们在第一单元中看到的),只是我们从现有的模型开始。让我们看看它是如何工作的,并在过程中讨论一些额外的注意事项。

首先,数据集:您可以尝试这个复古人脸数据集这些动漫人脸,以获得更接近这个面部模型原始训练数据的内容,但为了好玩,我们还是使用与第一单元中从头开始训练时使用的相同的小蝴蝶数据集。运行下面的代码下载蝴蝶数据集并创建一个数据加载器,我们可以从中采样一批图像

>>> # @markdown load and prepare a dataset:
>>> # Not on Colab? Comments with #@ enable UI tweaks like headings or user inputs
>>> # but can safely be ignored if you're working on a different platform.

>>> dataset_name = "huggan/smithsonian_butterflies_subset"  # @param
>>> dataset = load_dataset(dataset_name, split="train")
>>> image_size = 256  # @param
>>> batch_size = 4  # @param
>>> preprocess = transforms.Compose(
...     [
...         transforms.Resize((image_size, image_size)),
...         transforms.RandomHorizontalFlip(),
...         transforms.ToTensor(),
...         transforms.Normalize([0.5], [0.5]),
...     ]
... )


>>> def transform(examples):
...     images = [preprocess(image.convert("RGB")) for image in examples["image"]]
...     return {"images": images}


>>> dataset.set_transform(transform)

>>> train_dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)

>>> print("Previewing batch:")
>>> batch = next(iter(train_dataloader))
>>> grid = torchvision.utils.make_grid(batch["images"], nrow=4)
>>> plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5)
Previewing batch:

考虑因素 1:我们这里的批次大小(4)非常小,因为我们正在使用相当大的模型以大图像尺寸(256px)进行训练,如果我们将批次大小提高太多,就会耗尽 GPU 内存。您可以减小图像大小以加快速度并允许使用更大的批次,但这些模型最初的设计和训练都是为了生成 256px 的图像。

现在是训练循环。我们将通过将优化目标设置为image_pipe.unet.parameters()来更新预训练模型的权重。其余部分几乎与第一单元的示例训练循环相同。这在大约 10 分钟内就可以在 Colab 上运行,所以现在是您去喝杯咖啡或茶的好时机,一边等待

>>> num_epochs = 2  # @param
>>> lr = 1e-5  # 2param
>>> grad_accumulation_steps = 2  # @param

>>> optimizer = torch.optim.AdamW(image_pipe.unet.parameters(), lr=lr)

>>> losses = []

>>> for epoch in range(num_epochs):
...     for step, batch in tqdm(enumerate(train_dataloader), total=len(train_dataloader)):
...         clean_images = batch["images"].to(device)
...         # Sample noise to add to the images
...         noise = torch.randn(clean_images.shape).to(clean_images.device)
...         bs = clean_images.shape[0]

...         # Sample a random timestep for each image
...         timesteps = torch.randint(
...             0,
...             image_pipe.scheduler.num_train_timesteps,
...             (bs,),
...             device=clean_images.device,
...         ).long()

...         # Add noise to the clean images according to the noise magnitude at each timestep
...         # (this is the forward diffusion process)
...         noisy_images = image_pipe.scheduler.add_noise(clean_images, noise, timesteps)

...         # Get the model prediction for the noise
...         noise_pred = image_pipe.unet(noisy_images, timesteps, return_dict=False)[0]

...         # Compare the prediction with the actual noise:
...         loss = F.mse_loss(
...             noise_pred, noise
...         )  # NB - trying to predict noise (eps) not (noisy_ims-clean_ims) or just (clean_ims)

...         # Store for later plotting
...         losses.append(loss.item())

...         # Update the model parameters with the optimizer based on this loss
...         loss.backward(loss)

...         # Gradient accumulation:
...         if (step + 1) % grad_accumulation_steps == 0:
...             optimizer.step()
...             optimizer.zero_grad()

...     print(f"Epoch {epoch} average loss: {sum(losses[-len(train_dataloader):])/len(train_dataloader)}")

>>> # Plot the loss curve:
>>> plt.plot(losses)
Epoch 0 average loss: 0.013324214214226231

考虑因素 2:我们的损失信号非常嘈杂,因为我们每个步骤只使用四个随机噪声级别的示例。这不利于训练。一个解决方法是使用极低的学习率来限制每个步骤的更新大小。如果我们能找到某种方法来获得使用更大批次大小所能获得的相同好处,而无需内存需求激增,那就更好了……

引入梯度累积。如果我们在运行optimizer.step()optimizer.zero_grad()之前多次调用loss.backward(),那么 PyTorch 会累积(求和)梯度,有效地合并来自多个批次的信号以提供一个(更好的)估计,然后使用该估计来更新参数。这导致更新的总数减少,就像我们使用更大批次大小时看到的那样。许多框架都会为您处理此问题(例如,🤗 Accelerate 使这变得很容易),但从头开始实现它很好,因为这是一种处理 GPU 内存限制下训练的有用技术!从上面的代码(在# 梯度累积注释之后)可以看到,实际上不需要太多代码。

# Exercise: See if you can add gradient accumulation to the training loop in Unit 1.
# How does it perform? Think how you might adjust the learning rate based on the
# number of gradient accumulation steps - should it stay the same as before?

考虑因素 3:这仍然需要很多时间,并且每个 epoch 打印一行更新不足以提供良好的反馈,让我们了解正在发生的事情。我们可能应该

  • 偶尔生成一些样本以定性地直观检查模型训练期间的性能
  • 记录训练期间的损失和样本生成等内容,可能使用 Weights and Biases 或 tensorboard 等工具。

我创建了一个快速脚本(finetune_model.py),它采用上面的训练代码并添加最少的日志记录功能。您可以在下面看到此处一个训练运行的日志

%wandb johnowhitaker/dm_finetune/2upaa341 # You'll need a W&B account for this to work - skip if you don't want to log in

看到生成的样本在训练过程中如何变化很有趣——即使损失似乎没有太大改善,我们也可以看到从原始领域(卧室图像)到新训练数据(wikiart)的进展。在本笔记本的末尾,是使用此脚本微调模型的注释代码,作为运行上面单元格的替代方法。

# Exercise: see if you can modify the official example training script we saw
# in Unit 1 to begin with a pre-trained model rather than training from scratch.
# Compare it to the minimal script linked above - what extra features is the minimal script missing?

使用此模型生成一些图像,我们可以看到这些面孔已经看起来很奇怪了!

>>> # @markdown Generate and plot some images:
>>> x = torch.randn(8, 3, 256, 256).to(device)  # Batch of 8
>>> for i, t in tqdm(enumerate(scheduler.timesteps)):
...     model_input = scheduler.scale_model_input(x, t)
...     with torch.no_grad():
...         noise_pred = image_pipe.unet(model_input, t)["sample"]
...     x = scheduler.step(noise_pred, t, x).prev_sample
>>> grid = torchvision.utils.make_grid(x, nrow=4)
>>> plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5)

考虑因素 4:微调可能非常不可预测!如果我们训练更长时间,我们可能会看到一些完美的蝴蝶。但是中间步骤本身可能非常有趣,尤其是在您的兴趣更偏向艺术方面时!探索训练非常短或非常长的时间,并改变学习率以查看这如何影响最终模型产生的输出类型。

使用我们在 WikiArt 演示模型上使用的最小示例脚本微调模型的代码

如果您想训练一个类似于我在 WikiArt 上训练的模型,您可以取消注释并运行下面的单元格。由于这需要一段时间并且可能会耗尽您的 GPU 内存,因此我建议您完成本笔记本的其余部分之后再执行此操作。

## To download the fine-tuning script:
# !wget https://github.com/huggingface/diffusion-models-class/raw/main/unit2/finetune_model.py
## To run the script, training the face model on some vintage faces
## (ideally run this in a terminal):
# !python finetune_model.py --image_size 128 --batch_size 8 --num_epochs 16\
#     --grad_accumulation_steps 2 --start_model "google/ddpm-celebahq-256"\
#     --dataset_name "Norod78/Vintage-Faces-FFHQAligned" --wandb_project 'dm-finetune'\
#     --log_samples_every 100 --save_model_every 1000 --model_save_name 'vintageface'

保存和加载微调管道

现在我们已经微调了扩散模型中的 U-Net,让我们通过运行以下命令将其保存到本地文件夹

image_pipe.save_pretrained("my-finetuned-model")

正如我们在第一单元中看到的,这将保存配置、模型、调度程序

>>> !ls {"my-finetuned-model"}
model_index.json  scheduler  unet

接下来,您可以按照第一单元的Diffusers 简介中概述的相同步骤将模型推送到 Hub 以供以后使用

# @title Upload a locally saved pipeline to the hub

# Code to upload a pipeline saved locally to the hub
from huggingface_hub import HfApi, ModelCard, create_repo, get_full_repo_name

# Set up repo and upload files
model_name = "ddpm-celebahq-finetuned-butterflies-2epochs"  # @param What you want it called on the hub
local_folder_name = (
    "my-finetuned-model"  # @param Created by the script or one you created via image_pipe.save_pretrained('save_name')
)
description = "Describe your model here"  # @param
hub_model_id = get_full_repo_name(model_name)
create_repo(hub_model_id)
api = HfApi()
api.upload_folder(folder_path=f"{local_folder_name}/scheduler", path_in_repo="", repo_id=hub_model_id)
api.upload_folder(folder_path=f"{local_folder_name}/unet", path_in_repo="", repo_id=hub_model_id)
api.upload_file(
    path_or_fileobj=f"{local_folder_name}/model_index.json",
    path_in_repo="model_index.json",
    repo_id=hub_model_id,
)

# Add a model card (optional but nice!)
content = f"""
---
license: mit
tags:
- pytorch
- diffusers
- unconditional-image-generation
- diffusion-models-class
---

# Example Fine-Tuned Model for Unit 2 of the [Diffusion Models Class 🧨](https://github.com/huggingface/diffusion-models-class)

{description}

## Usage

```python
from diffusers import DDPMPipeline

pipeline = DDPMPipeline.from_pretrained('{hub_model_id}')
image = pipeline().images[0]
image

"""

card = ModelCard(content) card.push_to_hub(hub_model_id)


Congratulations, you've now fine-tuned your first diffusion model!

For the rest of this notebook we'll use a [model](https://huggingface.co/johnowhitaker/sd-class-wikiart-from-bedrooms) I fine-tuned from [this model trained on LSUN bedrooms](https://huggingface.co/google/ddpm-bedroom-256) approximately one epoch on the [WikiArt dataset](https://huggingface.co/datasets/huggan/wikiart). If you'd prefer, you can skip this cell and use the faces/butterflies pipeline we fine-tuned in the previous section or load one from the Hub instead:

```python
>>> # Load the pretrained pipeline
>>> pipeline_name = "johnowhitaker/sd-class-wikiart-from-bedrooms"
>>> image_pipe = DDPMPipeline.from_pretrained(pipeline_name).to(device)

>>> # Sample some images with a DDIM Scheduler over 40 steps
>>> scheduler = DDIMScheduler.from_pretrained(pipeline_name)
>>> scheduler.set_timesteps(num_inference_steps=40)

>>> # Random starting point (batch of 8 images)
>>> x = torch.randn(8, 3, 256, 256).to(device)

>>> # Minimal sampling loop
>>> for i, t in tqdm(enumerate(scheduler.timesteps)):
...     model_input = scheduler.scale_model_input(x, t)
...     with torch.no_grad():
...         noise_pred = image_pipe.unet(model_input, t)["sample"]
...     x = scheduler.step(noise_pred, t, x).prev_sample

>>> # View the results
>>> grid = torchvision.utils.make_grid(x, nrow=4)
>>> plt.imshow(grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5)

考虑因素 5:通常很难判断微调的效果如何,以及“良好性能”的含义可能因用例而异。例如,如果您正在对一个小数据集上的文本条件模型(如稳定扩散)进行微调,您可能希望它保留其大部分原始训练,以便它能够理解新数据集中未涵盖的任意提示,同时适应以更好地匹配新训练数据 的风格。这可能意味着使用低学习率以及指数模型平均等方法,如这篇关于创建口袋妖怪版本的稳定扩散的精彩博文中所演示的那样。在另一种情况下,您可能希望完全在新数据上重新训练模型(例如我们的卧室 -> wikiart 示例),在这种情况下,使用更大的学习率和更多训练是有意义的。即使损失图没有显示出明显的改进,但样本清楚地显示出从原始数据向更多“艺术化”输出的转变,尽管它们仍然大部分是不连贯的。

这将我们引向下一节,我们将探讨如何为这种模型添加其他指导,以便更好地控制输出……

指导

如果我们想要对生成的样本进行一些控制,该怎么办?例如,假设我们想使生成的图像偏向于特定的颜色。我们该如何做到这一点?引入**指导**,这是一种向采样过程添加额外控制的技术。

第一步是创建我们的条件函数:我们想要最小化的某种度量(损失)。以下是一个颜色示例,它将图像的像素与目标颜色(默认情况下为一种浅青绿色)进行比较并返回平均误差

def color_loss(images, target_color=(0.1, 0.9, 0.5)):
    """Given a target color (R, G, B) return a loss for how far away on average
    the images' pixels are from that color. Defaults to a light teal: (0.1, 0.9, 0.5)"""
    target = torch.tensor(target_color).to(images.device) * 2 - 1  # Map target color to (-1, 1)
    target = target[None, :, None, None]  # Get shape right to work with the images (b, c, h, w)
    error = torch.abs(images - target).mean()  # Mean absolute difference between the image pixels and the target color
    return error

接下来,我们将制作一个修改后的采样循环版本,在每个步骤中,我们将执行以下操作

  • 创建一个新的 x 版本,其中 requires_grad = True
  • 计算去噪版本 (x0)
  • 将预测的 x0 通过我们的损失函数
  • 找到此损失函数相对于 x 的**梯度**
  • 使用此条件梯度修改 x,然后我们使用调度程序进行步进,希望将 x 推向根据我们的指导函数导致损失更低的方向

这里有两个变体可以探索。在第一个变体中,我们在从 UNet 获取噪声预测之后为 x 设置 requires_grad,这在内存方面更有效(因为我们不必将梯度回溯到扩散模型),但会给出不太准确的梯度。在第二个变体中,我们首先为 x 设置 requires_grad,然后将其通过 UNet 并计算预测的 x0。

>>> # Variant 1: shortcut method

>>> # The guidance scale determines the strength of the effect
>>> guidance_loss_scale = 40  # Explore changing this to 5, or 100

>>> x = torch.randn(8, 3, 256, 256).to(device)

>>> for i, t in tqdm(enumerate(scheduler.timesteps)):

...     # Prepare the model input
...     model_input = scheduler.scale_model_input(x, t)

...     # predict the noise residual
...     with torch.no_grad():
...         noise_pred = image_pipe.unet(model_input, t)["sample"]

...     # Set x.requires_grad to True
...     x = x.detach().requires_grad_()

...     # Get the predicted x0
...     x0 = scheduler.step(noise_pred, t, x).pred_original_sample

...     # Calculate loss
...     loss = color_loss(x0) * guidance_loss_scale
...     if i % 10 == 0:
...         print(i, "loss:", loss.item())

...     # Get gradient
...     cond_grad = -torch.autograd.grad(loss, x)[0]

...     # Modify x based on this gradient
...     x = x.detach() + cond_grad

...     # Now step with scheduler
...     x = scheduler.step(noise_pred, t, x).prev_sample

>>> # View the output
>>> grid = torchvision.utils.make_grid(x, nrow=4)
>>> im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
>>> Image.fromarray(np.array(im * 255).astype(np.uint8))
0 loss: 27.279136657714844
10 loss: 11.286816596984863
20 loss: 10.683112144470215
30 loss: 10.942476272583008

第二个选项需要几乎双倍的 GPU 内存才能运行,即使我们只生成一批四个图像而不是八个。看看您是否能发现其中的区别,并思考为什么这种方法更“准确”

>>> # Variant 2: setting x.requires_grad before calculating the model predictions

>>> guidance_loss_scale = 40
>>> x = torch.randn(4, 3, 256, 256).to(device)

>>> for i, t in tqdm(enumerate(scheduler.timesteps)):

...     # Set requires_grad before the model forward pass
...     x = x.detach().requires_grad_()
...     model_input = scheduler.scale_model_input(x, t)

...     # predict (with grad this time)
...     noise_pred = image_pipe.unet(model_input, t)["sample"]

...     # Get the predicted x0:
...     x0 = scheduler.step(noise_pred, t, x).pred_original_sample

...     # Calculate loss
...     loss = color_loss(x0) * guidance_loss_scale
...     if i % 10 == 0:
...         print(i, "loss:", loss.item())

...     # Get gradient
...     cond_grad = -torch.autograd.grad(loss, x)[0]

...     # Modify x based on this gradient
...     x = x.detach() + cond_grad

...     # Now step with scheduler
...     x = scheduler.step(noise_pred, t, x).prev_sample


>>> grid = torchvision.utils.make_grid(x, nrow=4)
>>> im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
>>> Image.fromarray(np.array(im * 255).astype(np.uint8))
0 loss: 30.750328063964844
10 loss: 18.550724029541016
20 loss: 17.515094757080078
30 loss: 17.55681037902832

在第二个变体中,内存需求更高,效果不那么明显,因此您可能会认为它不如第一个变体。但是,输出可以说更接近模型训练的图像类型,并且您始终可以增加指导比例以获得更强的效果。最终使用哪种方法取决于哪种方法在实验中效果最好。

# Exercise: pick your favourite colour and look up it's values in RGB space.
# Edit the `color_loss()` line in the cell above to receive these new RGB values and examine the outputs - do they match what you expect?

CLIP 指导

朝某个颜色进行引导给了我们一点控制力,但如果我们可以直接输入一些文本描述我们想要的内容呢?

CLIP 是 OpenAI 创建的一个模型,它允许我们将图像与文本标题进行比较。这非常强大,因为它允许我们量化图像与提示匹配的程度。并且由于该过程是可微的,我们可以将其用作损失函数来指导我们的扩散模型!

我们不会在这里过多地深入细节。基本方法如下

  • 嵌入文本提示以获取文本的 512 维 CLIP 嵌入
  • 对于扩散模型过程中的每个步骤
    • 对预测的去噪图像进行多种变体(有多种变体可以提供更清晰的损失信号)
    • 对于每个变体,使用 CLIP 嵌入图像并将此嵌入与提示的文本嵌入进行比较(使用称为“大圆距离平方”的度量)
  • 计算此损失相对于当前噪声 x 的梯度,并在使用调度器更新 x 之前使用此梯度修改 x。

有关 CLIP 的更深入解释,请查看关于该主题的课程关于我们用于加载 CLIP 模型的 OpenCLIP 项目的报告。运行下一个单元格以加载 CLIP 模型

# @markdown load a CLIP model and define the loss function
import open_clip

clip_model, _, preprocess = open_clip.create_model_and_transforms("ViT-B-32", pretrained="openai")
clip_model.to(device)

# Transforms to resize and augment an image + normalize to match CLIP's training data
tfms = torchvision.transforms.Compose(
    [
        torchvision.transforms.RandomResizedCrop(224),  # Random CROP each time
        torchvision.transforms.RandomAffine(5),  # One possible random augmentation: skews the image
        torchvision.transforms.RandomHorizontalFlip(),  # You can add additional augmentations if you like
        torchvision.transforms.Normalize(
            mean=(0.48145466, 0.4578275, 0.40821073),
            std=(0.26862954, 0.26130258, 0.27577711),
        ),
    ]
)


# And define a loss function that takes an image, embeds it and compares with
# the text features of the prompt
def clip_loss(image, text_features):
    image_features = clip_model.encode_image(tfms(image))  # Note: applies the above transforms
    input_normed = torch.nn.functional.normalize(image_features.unsqueeze(1), dim=2)
    embed_normed = torch.nn.functional.normalize(text_features.unsqueeze(0), dim=2)
    dists = input_normed.sub(embed_normed).norm(dim=2).div(2).arcsin().pow(2).mul(2)  # Squared Great Circle Distance
    return dists.mean()

在定义了损失函数后,我们的引导采样循环看起来类似于前面的示例,用我们新的基于剪辑的损失函数替换color_loss()

>>> # @markdown applying guidance using CLIP

>>> prompt = "Red Rose (still life), red flower painting"  # @param

>>> # Explore changing this
>>> guidance_scale = 8  # @param
>>> n_cuts = 4  # @param

>>> # More steps -> more time for the guidance to have an effect
>>> scheduler.set_timesteps(50)

>>> # We embed a prompt with CLIP as our target
>>> text = open_clip.tokenize([prompt]).to(device)
>>> with torch.no_grad(), torch.cuda.amp.autocast():
...     text_features = clip_model.encode_text(text)


>>> x = torch.randn(4, 3, 256, 256).to(device)  # RAM usage is high, you may want only 1 image at a time

>>> for i, t in tqdm(enumerate(scheduler.timesteps)):

...     model_input = scheduler.scale_model_input(x, t)

...     # predict the noise residual
...     with torch.no_grad():
...         noise_pred = image_pipe.unet(model_input, t)["sample"]

...     cond_grad = 0

...     for cut in range(n_cuts):

...         # Set requires grad on x
...         x = x.detach().requires_grad_()

...         # Get the predicted x0:
...         x0 = scheduler.step(noise_pred, t, x).pred_original_sample

...         # Calculate loss
...         loss = clip_loss(x0, text_features) * guidance_scale

...         # Get gradient (scale by n_cuts since we want the average)
...         cond_grad -= torch.autograd.grad(loss, x)[0] / n_cuts

...     if i % 25 == 0:
...         print("Step:", i, ", Guidance loss:", loss.item())

...     # Modify x based on this gradient
...     alpha_bar = scheduler.alphas_cumprod[i]
...     x = x.detach() + cond_grad * alpha_bar.sqrt()  # Note the additional scaling factor here!

...     # Now step with scheduler
...     x = scheduler.step(noise_pred, t, x).prev_sample


>>> grid = torchvision.utils.make_grid(x.detach(), nrow=4)
>>> im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
>>> Image.fromarray(np.array(im * 255).astype(np.uint8))
Step: 0 , Guidance loss: 7.437869548797607
Step: 25 , Guidance loss: 7.174620628356934

这些看起来有点像玫瑰!它并不完美,但如果你调整设置,你可以用它得到一些令人愉悦的图像。

如果你检查上面的代码,你会发现我将条件梯度按alpha_bar.sqrt()因子进行缩放。有一些理论表明缩放这些梯度的“正确”方法,但在实践中,这也是你可以尝试的东西。对于某些类型的引导,你可能希望大部分效果集中在早期步骤中,对于其他引导(例如,专注于纹理的样式损失),你可能希望它们只在生成过程的后期开始发挥作用。下面显示了一些可能的调度。

>>> # @markdown Plotting some possible schedules:
>>> plt.plot([1 for a in scheduler.alphas_cumprod], label="no scaling")
>>> plt.plot([a for a in scheduler.alphas_cumprod], label="alpha_bar")
>>> plt.plot([a.sqrt() for a in scheduler.alphas_cumprod], label="alpha_bar.sqrt()")
>>> plt.plot([(1 - a).sqrt() for a in scheduler.alphas_cumprod], label="(1-alpha_bar).sqrt()")
>>> plt.legend()
>>> plt.title("Possible guidance scaling schedules")

尝试不同的调度、引导尺度以及你能想到的任何其他技巧(在某个范围内剪辑梯度是一种流行的修改),看看你能得到多好的结果!还要确保尝试换用其他模型。也许是我们一开始加载的人脸模型——你能可靠地引导它生成男性面孔吗?如果你将 CLIP 指导与我们之前使用的颜色损失相结合会怎样?等等。

如果你查看一些在实践中使用 CLIP 指导扩散的代码,你会看到一种更复杂的方法,它有一个更好的类用于从图像中挑选随机剪裁,并且对损失函数进行了大量额外的调整以提高性能。在文本条件扩散模型出现之前,这是当时最好的文本到图像系统!我们这里的小型玩具版本还有很大的改进空间,但它抓住了核心思想:由于引导加上 CLIP 的惊人能力,我们可以为无条件扩散模型添加文本控制🎨。

将自定义采样循环作为 Gradio 演示共享

也许你已经想出了一个有趣的损失来引导生成,并且现在你希望与世界分享你微调的模型和这种自定义采样策略……

进入Gradio。Gradio 是一款免费的开源工具,允许用户通过简单的 Web 界面轻松创建和共享交互式机器学习模型。使用 Gradio,用户可以为其机器学习模型构建自定义界面,然后可以通过唯一的 URL 与他人共享。它也集成到 🤗 Spaces 中,这使得托管演示和与他人共享变得很容易。

我们将核心逻辑放在一个函数中,该函数接收一些输入并生成图像作为输出。然后,它可以包装在一个简单的界面中,允许用户指定一些参数(作为输入传递给主生成函数)。有许多组件可用——对于此示例,我们将使用一个滑块来设置引导尺度,并使用一个颜色选择器来定义目标颜色。

%pip install -q gradio # Install the library
import gradio as gr
from PIL import Image, ImageColor


# The function that does the hard work
def generate(color, guidance_loss_scale):
    target_color = ImageColor.getcolor(color, "RGB")  # Target color as RGB
    target_color = [a / 255 for a in target_color]  # Rescale from (0, 255) to (0, 1)
    x = torch.randn(1, 3, 256, 256).to(device)
    for i, t in tqdm(enumerate(scheduler.timesteps)):
        model_input = scheduler.scale_model_input(x, t)
        with torch.no_grad():
            noise_pred = image_pipe.unet(model_input, t)["sample"]
        x = x.detach().requires_grad_()
        x0 = scheduler.step(noise_pred, t, x).pred_original_sample
        loss = color_loss(x0, target_color) * guidance_loss_scale
        cond_grad = -torch.autograd.grad(loss, x)[0]
        x = x.detach() + cond_grad
        x = scheduler.step(noise_pred, t, x).prev_sample
    grid = torchvision.utils.make_grid(x, nrow=4)
    im = grid.permute(1, 2, 0).cpu().clip(-1, 1) * 0.5 + 0.5
    im = Image.fromarray(np.array(im * 255).astype(np.uint8))
    im.save("test.jpeg")
    return im


# See the gradio docs for the types of inputs and outputs available
inputs = [
    gr.ColorPicker(label="color", value="55FFAA"),  # Add any inputs you need here
    gr.Slider(label="guidance_scale", minimum=0, maximum=30, value=3),
]
outputs = gr.Image(label="result")

# And the minimal interface
demo = gr.Interface(
    fn=generate,
    inputs=inputs,
    outputs=outputs,
    examples=[
        ["#BB2266", 3],
        ["#44CCAA", 5],  # You can provide some example inputs to get people started
    ],
)
demo.launch(debug=True)  # debug=True allows you to see errors and output in Colab

可以构建更复杂的界面,具有花哨的样式和各种可能的输入,但对于此演示,我们将保持尽可能简单。

默认情况下,🤗 Spaces 上的演示在 CPU 上运行,因此在迁移之前,在 Colab 中(如上所述)对你的界面进行原型设计会很好。当你准备好共享你的演示时,你将创建一个空间,设置一个requirements.txt文件列出你的代码将使用的库,然后将所有代码放在一个app.py文件中,该文件定义了相关的函数和界面。

Screenshot from 2022-12-11 10-28-26.png

幸运的是,还有一个“复制”空间的选项。你可以访问我的演示空间此处(如上所示)并点击“复制此空间”以获取一个模板,然后你可以修改它以使用你自己的模型和引导函数。

在设置中,你可以配置你的空间以在更高级的硬件上运行(按小时收费)。制作了一些很棒的东西并想在更好的硬件上共享,但没有钱?通过 Discord 告知我们,我们会看看是否可以提供帮助!

总结和后续步骤

我们在本笔记本中涵盖了很多内容!让我们回顾一下核心思想

  • 加载现有模型并使用不同的调度器对其进行采样相对容易
  • 微调看起来就像从头开始训练一样,只是通过从现有模型开始,我们希望更快地获得更好的结果
  • 为了在大型图像上微调大型模型,我们可以使用诸如梯度累积之类的技巧来解决批大小限制
  • 记录样本图像对于微调很重要,在微调中,损失曲线可能不会显示太多有用的信息
  • 引导允许我们采用无条件模型并根据某些引导/损失函数来引导生成过程,在每个步骤中,我们找到损失相对于噪声图像 x 的梯度,并在继续下一个时间步之前根据此梯度对其进行更新
  • 使用 CLIP 进行引导使我们可以用文本控制无条件模型!

为了将此付诸实践,以下是一些你可以采取的具体后续步骤

  • 微调你自己的模型并将其推送到中心。这将涉及选择一个起点(例如,在人脸卧室上面提到的 wikiart 示例上训练的模型)和一个数据集(可能是这些动物面孔或你自己的图像),然后运行本笔记本中的代码或示例脚本(下面演示用法)。
  • 探索使用微调模型进行引导,可以使用示例引导函数(color_loss 或 CLIP)之一,也可以发明你自己的引导函数。
  • 使用 Gradio 共享基于此的演示,要么修改示例空间以使用你自己的模型,要么创建你自己的具有更多功能的自定义版本。

我们期待在 Discord、Twitter 和其他地方看到你的成果 🤗!