扩散模型课程文档
微调和引导
并获得增强的文档体验
开始使用
微调和引导
在本笔记本中,我们将介绍两种用于调整现有扩散模型的主要方法
- 通过**微调**,我们将在新数据上重新训练现有模型,以改变它们生成的输出类型
- 通过**引导**,我们将在推理时引导现有模型的生成过程,以实现额外的控制
你将学到什么:
学完本笔记本,你将知道如何
- 创建一个采样循环并使用新的调度器更快地生成样本
- 在新数据上微调现有的扩散模型,包括
- 使用梯度累积来解决小批量带来的一些问题
- 在训练期间将样本记录到 Weights and Biases 以监控进度(通过附带的示例脚本)
- 保存生成的 pipeline 并将其上传到 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"
加载预训练的 Pipeline
要开始本笔记本,让我们加载一个现有的 pipeline,看看我们可以用它做什么
image_pipe = DDPMPipeline.from_pretrained("google/ddpm-celebahq-256")
image_pipe.to(device)
生成图像就像运行 pipeline 的 __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
你也可以用这个新的调度器替换 pipeline 自带的原始调度器,然后像这样进行采样
>>> image_pipe.scheduler = scheduler
>>> images = image_pipe(num_inference_steps=40).images
>>> images[0]
好了——我们现在可以在合理的时间内获得样本了!在我们继续本笔记本的其余部分时,这应该会加快速度 :)
微调
现在是有趣的部分了!给定这个预训练的 pipeline,我们如何重新训练模型以根据新的训练数据生成图像?
事实证明,这看起来几乎与从零开始训练模型(正如我们在 第一单元 中看到的那样)完全相同,只是我们从现有模型开始。让我们看看实际操作,并在此过程中讨论一些额外的考虑因素。
首先是数据集:你可以尝试这个复古人脸数据集或这些动漫人脸,它们更接近这个面部模型的原始训练数据,但为了好玩,我们还是使用我们在第一单元中从零开始训练时用过的同一个小型蝴蝶数据集。运行下面的代码以下载蝴蝶数据集并创建一个我们可以从中采样一批图像的数据加载器
>>> # @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 RAM。你可以减小图像尺寸来加快速度并允许更大的批次,但这些模型是为 256px 生成而设计和最初训练的。
现在是训练循环。我们将通过将优化目标设置为 `image_pipe.unet.parameters()` 来更新预训练模型的权重。其余部分与第一单元的示例训练循环几乎相同。这在 Colab 上运行大约需要 10 分钟,所以现在是喝杯咖啡或茶的好时机
>>> 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 内存限制下训练的有用技术!正如你从上面的代码(在 `# Gradient accumulation` 注释之后)可以看到的,实际上并不需要太多代码。
# 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'
保存和加载微调后的 Pipeline
现在我们已经微调了扩散模型中的 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: 通常很难判断微调的效果如何,以及“良好性能”的含义可能因用例而异。例如,如果你在一个小数据集上微调像 Stable Diffusion 这样的文本条件模型,你可能希望它**保留**大部分原始训练,以便它能理解新数据集未涵盖的任意提示,同时**适应**以更好地匹配新训练数据的风格。这可能意味着使用低学习率以及像指数模型平均这样的方法,正如这篇关于创建 Stable Diffusion 口袋妖怪版本的精彩博客文章中所演示的那样。在不同的情况下,你可能希望在新数据上完全重新训练一个模型(例如我们的卧室 -> 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 RAM 才能运行,即使我们只生成一批四个图像而不是八个。看看你是否能发现差异,并思考为什么这种方式更“准确”
>>> # 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,请查看关于该主题的这节课或这篇关于 OpenCLIP 项目的报告,我们正在使用它来加载 CLIP 模型。运行下一个单元格以加载 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()
定义了损失函数后,我们的引导采样循环看起来与之前的示例类似,只是用我们新的基于 CLIP 的损失函数替换了 `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 是一个免费的开源工具,允许用户通过简单的网页界面轻松创建和分享交互式机器学习模型。使用 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 中(如上所示)原型化你的界面是很好的。当你准备好分享你的演示时,你将创建一个 Space,设置一个 `requirements.txt` 文件列出你的代码将使用的库,然后将所有代码放在一个 `app.py` 文件中,该文件定义了相关函数和界面。
幸运的是,还有一个“复制” Space 的选项。你可以访问我的演示空间这里(如上所示),然后点击“复制此 Space”以获得一个模板,然后你可以修改它以使用你自己的模型和引导函数。
在设置中,你可以配置你的 Space 以在更高级的硬件上运行(按小时收费)。做出了一些惊人的东西,想在更好的硬件上分享但没有钱?通过 Discord 告诉我们,我们会看看是否能提供帮助!
总结与后续步骤
我们在本笔记本中涵盖了很多内容!让我们回顾一下核心思想
- 加载现有模型并使用不同的调度器进行采样相对容易
- 微调看起来就像从头开始训练,只是通过从现有模型开始,我们希望更快地获得更好的结果
- 要在大图像上微调大模型,我们可以使用梯度累积等技巧来规避批次大小的限制
- 对于微调来说,记录样本图像很重要,因为损失曲线可能不会显示太多有用的信息
- 引导允许我们采用一个无条件模型,并根据某个引导/损失函数来引导生成过程,其中在每一步,我们找到损失相对于带噪声图像 x 的梯度,并在进入下一个时间步之前根据此梯度更新它
- 使用 CLIP 引导让我们能用文本控制无条件模型!
为了将这些付诸实践,以下是你可以采取的一些具体后续步骤
- 微调你自己的模型并将其推送到 Hub。这将涉及选择一个起点(例如,一个在人脸、卧室、猫或上面的 wikiart 示例上训练的模型)和一个数据集(也许是这些动物面孔或你自己的图像),然后运行本笔记本中的代码或示例脚本(下面是演示用法)。
- 使用你微调的模型探索引导,可以使用示例引导函数之一(color_loss 或 CLIP),或者发明你自己的。
- 使用 Gradio 分享一个基于此的演示,可以修改示例空间以使用你自己的模型,或者创建你自己的具有更多功能的自定义版本。
我们期待在 Discord、Twitter 和其他地方看到你的成果 🤗!
< > 在 GitHub 上更新