Diffusers 文档
评估扩散模型
并获得增强的文档体验
开始使用
评估扩散模型
鉴于现有图像生成扩散模型评估框架的出现,本文档现已过时。请查看 HEIM、T2I-Compbench、GenEval 等工作。
Stable Diffusion 等生成模型的评估本质上是主观的。但作为实践者和研究人员,我们常常需要在众多可能性中做出审慎的选择。那么,在使用不同的生成模型(如 GAN、扩散模型等)时,我们如何选择其中一个呢?
对这类模型进行定性评估可能容易出错,并可能错误地影响决策。然而,定量指标不一定与图像质量相对应。因此,通常情况下,定性评估和定量评估的结合在选择模型时能提供更强的信号。
在本文档中,我们对评估扩散模型的定性和定量方法进行了非详尽的概述。对于定量方法,我们特别关注如何在 diffusers
中实现它们。
本文档中所示的方法也可用于评估不同的噪声调度器,同时保持底层生成模型固定。
场景
我们涵盖了具有以下流水线的扩散模型
- 文本引导图像生成(例如
StableDiffusionPipeline
)。 - 文本引导图像生成,附加条件是输入图像(例如
StableDiffusionImg2ImgPipeline
和StableDiffusionInstructPix2PixPipeline
)。 - 类别条件图像生成模型(例如
DiTPipeline
)。
定性评估
定性评估通常涉及对生成图像的人工评估。质量通过组合性、图像-文本对齐和空间关系等方面进行衡量。常用提示为定性指标提供了一定程度的统一性。DrawBench 和 PartiPrompts 是用于定性基准测试的提示数据集。DrawBench 和 PartiPrompts 分别由 Imagen 和 Parti 引入。
来自 Parti 官方网站
PartiPrompts (P2) 是我们作为这项工作的一部分发布的一组丰富的、超过 1600 个英文提示。P2 可用于衡量模型在各种类别和挑战方面的能力。
PartiPrompts 包含以下列
- 提示
- 提示的类别(例如“抽象”、“世界知识”等)
- 反映难度的挑战(例如“基本”、“复杂”、“写作与符号”等)
这些基准允许对不同的图像生成模型进行并排的人工评估。
为此,🧨 Diffusers 团队构建了 Open Parti Prompts,这是一个社区驱动的基于 Parti Prompts 的定性基准,用于比较最先进的开源扩散模型
- Open Parti Prompts Game:对于 10 个 Parti 提示,显示 4 个生成的图像,用户选择最适合提示的图像。
- Open Parti Prompts 排行榜:比较当前最佳开源扩散模型的排行榜。
要手动比较图像,让我们看看如何在几个 PartiPrompts 上使用 diffusers
。
下面我们展示了一些在不同挑战中抽样的提示:基本、复杂、语言结构、想象力和写作与符号。这里我们使用 PartiPrompts 作为数据集。
from datasets import load_dataset
# prompts = load_dataset("nateraw/parti-prompts", split="train")
# prompts = prompts.shuffle()
# sample_prompts = [prompts[i]["Prompt"] for i in range(5)]
# Fixing these sample prompts in the interest of reproducibility.
sample_prompts = [
"a corgi",
"a hot air balloon with a yin-yang symbol, with the moon visible in the daytime sky",
"a car with no windows",
"a cube made of porcupine",
'The saying "BE EXCELLENT TO EACH OTHER" written on a red brick wall with a graffiti image of a green alien wearing a tuxedo. A yellow fire hydrant is on a sidewalk in the foreground.',
]
现在我们可以使用这些提示来生成一些图像,使用 Stable Diffusion (v1-4 checkpoint)
import torch
seed = 0
generator = torch.manual_seed(seed)
images = sd_pipeline(sample_prompts, num_images_per_prompt=1, generator=generator).images
我们还可以相应地设置 `num_images_per_prompt`,以比较同一提示的不同图像。运行相同的流水线,但使用不同的检查点 (v1-5),结果如下:
一旦使用多个模型(在评估中)从所有提示生成了多张图像,这些结果将呈现给人类评估员进行评分。有关 DrawBench 和 PartiPrompts 基准的更多详细信息,请参阅其各自的论文。
在模型训练时查看一些推理样本以衡量训练进度是很有用的。在我们的训练脚本中,我们支持此实用程序,并额外支持日志记录到 TensorBoard 和 Weights & Biases。
定量评估
在本节中,我们将向您介绍如何使用以下方法评估三种不同的扩散管道
- CLIP 分数
- CLIP 方向相似度
- FID
文本引导图像生成
CLIP 分数衡量图像-字幕对的兼容性。CLIP 分数越高意味着兼容性越高 🔼。CLIP 分数是定性概念“兼容性”的定量测量。图像-字幕对兼容性也可以看作是图像和字幕之间的语义相似度。CLIP 分数被发现与人类判断具有高度相关性。
我们先加载一个 StableDiffusionPipeline
from diffusers import StableDiffusionPipeline
import torch
model_ckpt = "CompVis/stable-diffusion-v1-4"
sd_pipeline = StableDiffusionPipeline.from_pretrained(model_ckpt, torch_dtype=torch.float16).to("cuda")
用多个提示生成一些图像
prompts = [
"a photo of an astronaut riding a horse on mars",
"A high tech solarpunk utopia in the Amazon rainforest",
"A pikachu fine dining with a view to the Eiffel Tower",
"A mecha robot in a favela in expressionist style",
"an insect robot preparing a delicious meal",
"A small cabin on top of a snowy mountain in the style of Disney, artstation",
]
images = sd_pipeline(prompts, num_images_per_prompt=1, output_type="np").images
print(images.shape)
# (6, 512, 512, 3)
然后,我们计算 CLIP 分数。
from torchmetrics.functional.multimodal import clip_score
from functools import partial
clip_score_fn = partial(clip_score, model_name_or_path="openai/clip-vit-base-patch16")
def calculate_clip_score(images, prompts):
images_int = (images * 255).astype("uint8")
clip_score = clip_score_fn(torch.from_numpy(images_int).permute(0, 3, 1, 2), prompts).detach()
return round(float(clip_score), 4)
sd_clip_score = calculate_clip_score(images, prompts)
print(f"CLIP score: {sd_clip_score}")
# CLIP score: 35.7038
在上面的示例中,我们为每个提示生成了一张图像。如果我们为每个提示生成了多张图像,我们就需要取每张图像的平均分数。
现在,如果我们要比较两个与 StableDiffusionPipeline 兼容的检查点,我们应该在调用流水线时传入一个生成器。首先,我们使用 v1-4 Stable Diffusion 检查点以固定种子生成图像
seed = 0
generator = torch.manual_seed(seed)
images = sd_pipeline(prompts, num_images_per_prompt=1, generator=generator, output_type="np").images
然后我们加载 v1-5 checkpoint 来生成图像
model_ckpt_1_5 = "stable-diffusion-v1-5/stable-diffusion-v1-5"
sd_pipeline_1_5 = StableDiffusionPipeline.from_pretrained(model_ckpt_1_5, torch_dtype=torch.float16).to("cuda")
images_1_5 = sd_pipeline_1_5(prompts, num_images_per_prompt=1, generator=generator, output_type="np").images
最后,我们比较它们的 CLIP 分数
sd_clip_score_1_4 = calculate_clip_score(images, prompts)
print(f"CLIP Score with v-1-4: {sd_clip_score_1_4}")
# CLIP Score with v-1-4: 34.9102
sd_clip_score_1_5 = calculate_clip_score(images_1_5, prompts)
print(f"CLIP Score with v-1-5: {sd_clip_score_1_5}")
# CLIP Score with v-1-5: 36.2137
v1-5 检查点似乎比其前身表现更好。然而,请注意,我们用于计算 CLIP 分数的提示数量相当少。对于更实际的评估,这个数量应该更高,并且提示应该多样化。
从结构上讲,这个分数存在一些局限性。训练数据集中的字幕是从网络上爬取并从互联网上与图像相关的`alt`和类似标签中提取的。它们不一定能代表人类会用来描述图像的词语。因此,我们不得不在这里“设计”一些提示。
图像条件文本到图像生成
在这种情况下,我们以输入图像和文本提示作为条件来生成流水线。以StableDiffusionInstructPix2PixPipeline为例。它将编辑指令作为输入提示,并将输入图像作为要编辑的对象。
这是一个例子
评估此类模型的一种策略是衡量两幅图像(在 CLIP 空间中)之间变化的**一致性**,以及两个图像标题之间变化的**一致性**(如 CLIP-Guided Domain Adaptation of Image Generators 所示)。这被称为“**CLIP 方向相似度**”。
- 标题 1 对应于要编辑的输入图像(图像 1)。
- 标题 2 对应于编辑后的图像(图像 2)。它应该反映编辑指令。
以下是图示概述
我们准备了一个小型数据集来实现此指标。首先加载数据集。
from datasets import load_dataset
dataset = load_dataset("sayakpaul/instructpix2pix-demo", split="train")
dataset.features
{'input': Value(dtype='string', id=None),
'edit': Value(dtype='string', id=None),
'output': Value(dtype='string', id=None),
'image': Image(decode=True, id=None)}
这里我们有
input
是与image
对应的标题。edit
表示编辑指令。output
表示反映edit
指令的修改后的标题。
我们来看一个样本。
idx = 0
print(f"Original caption: {dataset[idx]['input']}")
print(f"Edit instruction: {dataset[idx]['edit']}")
print(f"Modified caption: {dataset[idx]['output']}")
Original caption: 2. FAROE ISLANDS: An archipelago of 18 mountainous isles in the North Atlantic Ocean between Norway and Iceland, the Faroe Islands has 'everything you could hope for', according to Big 7 Travel. It boasts 'crystal clear waterfalls, rocky cliffs that seem to jut out of nowhere and velvety green hills'
Edit instruction: make the isles all white marble
Modified caption: 2. WHITE MARBLE ISLANDS: An archipelago of 18 mountainous white marble isles in the North Atlantic Ocean between Norway and Iceland, the White Marble Islands has 'everything you could hope for', according to Big 7 Travel. It boasts 'crystal clear waterfalls, rocky cliffs that seem to jut out of nowhere and velvety green hills'
这是图像
dataset[idx]["image"]
我们首先使用编辑指令编辑数据集中的图像并计算方向相似性。
我们先加载 StableDiffusionInstructPix2PixPipeline
from diffusers import StableDiffusionInstructPix2PixPipeline
instruct_pix2pix_pipeline = StableDiffusionInstructPix2PixPipeline.from_pretrained(
"timbrooks/instruct-pix2pix", torch_dtype=torch.float16
).to("cuda")
现在,我们执行编辑
import numpy as np
def edit_image(input_image, instruction):
image = instruct_pix2pix_pipeline(
instruction,
image=input_image,
output_type="np",
generator=generator,
).images[0]
return image
input_images = []
original_captions = []
modified_captions = []
edited_images = []
for idx in range(len(dataset)):
input_image = dataset[idx]["image"]
edit_instruction = dataset[idx]["edit"]
edited_image = edit_image(input_image, edit_instruction)
input_images.append(np.array(input_image))
original_captions.append(dataset[idx]["input"])
modified_captions.append(dataset[idx]["output"])
edited_images.append(edited_image)
为了衡量方向相似性,我们首先加载 CLIP 的图像和文本编码器
from transformers import (
CLIPTokenizer,
CLIPTextModelWithProjection,
CLIPVisionModelWithProjection,
CLIPImageProcessor,
)
clip_id = "openai/clip-vit-large-patch14"
tokenizer = CLIPTokenizer.from_pretrained(clip_id)
text_encoder = CLIPTextModelWithProjection.from_pretrained(clip_id).to("cuda")
image_processor = CLIPImageProcessor.from_pretrained(clip_id)
image_encoder = CLIPVisionModelWithProjection.from_pretrained(clip_id).to("cuda")
请注意,我们使用的是特定的 CLIP 检查点,即 `openai/clip-vit-large-patch14`。这是因为 Stable Diffusion 预训练是使用此 CLIP 变体进行的。有关更多详细信息,请参阅 文档。
接下来,我们准备一个 PyTorch `nn.Module` 来计算方向相似性。
import torch.nn as nn
import torch.nn.functional as F
class DirectionalSimilarity(nn.Module):
def __init__(self, tokenizer, text_encoder, image_processor, image_encoder):
super().__init__()
self.tokenizer = tokenizer
self.text_encoder = text_encoder
self.image_processor = image_processor
self.image_encoder = image_encoder
def preprocess_image(self, image):
image = self.image_processor(image, return_tensors="pt")["pixel_values"]
return {"pixel_values": image.to("cuda")}
def tokenize_text(self, text):
inputs = self.tokenizer(
text,
max_length=self.tokenizer.model_max_length,
padding="max_length",
truncation=True,
return_tensors="pt",
)
return {"input_ids": inputs.input_ids.to("cuda")}
def encode_image(self, image):
preprocessed_image = self.preprocess_image(image)
image_features = self.image_encoder(**preprocessed_image).image_embeds
image_features = image_features / image_features.norm(dim=1, keepdim=True)
return image_features
def encode_text(self, text):
tokenized_text = self.tokenize_text(text)
text_features = self.text_encoder(**tokenized_text).text_embeds
text_features = text_features / text_features.norm(dim=1, keepdim=True)
return text_features
def compute_directional_similarity(self, img_feat_one, img_feat_two, text_feat_one, text_feat_two):
sim_direction = F.cosine_similarity(img_feat_two - img_feat_one, text_feat_two - text_feat_one)
return sim_direction
def forward(self, image_one, image_two, caption_one, caption_two):
img_feat_one = self.encode_image(image_one)
img_feat_two = self.encode_image(image_two)
text_feat_one = self.encode_text(caption_one)
text_feat_two = self.encode_text(caption_two)
directional_similarity = self.compute_directional_similarity(
img_feat_one, img_feat_two, text_feat_one, text_feat_two
)
return directional_similarity
现在,让我们使用 `DirectionalSimilarity`。
dir_similarity = DirectionalSimilarity(tokenizer, text_encoder, image_processor, image_encoder)
scores = []
for i in range(len(input_images)):
original_image = input_images[i]
original_caption = original_captions[i]
edited_image = edited_images[i]
modified_caption = modified_captions[i]
similarity_score = dir_similarity(original_image, edited_image, original_caption, modified_caption)
scores.append(float(similarity_score.detach().cpu()))
print(f"CLIP directional similarity: {np.mean(scores)}")
# CLIP directional similarity: 0.0797976553440094
与 CLIP 分数一样,CLIP 方向相似度越高越好。
需要注意的是,`StableDiffusionInstructPix2PixPipeline` 暴露了两个参数,即 `image_guidance_scale` 和 `guidance_scale`,它们允许您控制最终编辑图像的质量。我们鼓励您尝试这两个参数,看看它们对方向相似度的影响。
我们可以将此度量的思想扩展到衡量原始图像和编辑版本之间的相似程度。为此,我们只需执行 `F.cosine_similarity(img_feat_two, img_feat_one)`。对于此类编辑,我们仍然希望尽可能保留图像的主要语义,即获得高相似度分数。
我们可以将这些指标用于类似的流水线,例如 StableDiffusionPix2PixZeroPipeline
。
CLIP 分数和 CLIP 方向相似度都依赖于 CLIP 模型,这可能会使评估产生偏差。
当评估中的模型是使用大型图像-字幕数据集(例如 LAION-5B 数据集)进行预训练时,**扩展 IS、FID(稍后讨论)或 KID 等指标可能会很困难**。这是因为这些指标的基础是一个用于提取中间图像特征的 InceptionNet(在 ImageNet-1k 数据集上预训练)。Stable Diffusion 的预训练数据集可能与 InceptionNet 的预训练数据集重叠有限,因此它不是一个好的特征提取候选。
使用上述指标有助于评估类别条件模型。例如,DiT。它是在 ImageNet-1k 类别上预训练的。
类别条件图像生成
类别条件生成模型通常在 ImageNet-1k 等带类别标签的数据集上进行预训练。评估这些模型的常用指标包括 Fréchet Inception 距离 (FID)、Kernel Inception 距离 (KID) 和 Inception 分数 (IS)。在本文档中,我们重点介绍 FID (Heusel et al.)。我们将展示如何使用 DiTPipeline
来计算 FID,该流水线底层使用 DiT 模型。
FID 旨在衡量两个图像数据集的相似程度。根据 此资源
Fréchet Inception Distance 是衡量两个图像数据集相似程度的指标。它已被证明与人类对视觉质量的判断高度相关,最常用于评估生成对抗网络的样本质量。FID 的计算方法是:计算拟合到 Inception 网络特征表示的两个高斯分布之间的 Fréchet 距离。
这两个数据集本质上是真实图像数据集和虚假图像数据集(在我们的例子中是生成的图像)。FID 通常使用两个大型数据集进行计算。但是,对于本文档,我们将使用两个小型数据集。
我们先从 ImageNet-1k 训练集中下载一些图像
from zipfile import ZipFile
import requests
def download(url, local_filepath):
r = requests.get(url)
with open(local_filepath, "wb") as f:
f.write(r.content)
return local_filepath
dummy_dataset_url = "https://huggingface.co/datasets/sayakpaul/sample-datasets/resolve/main/sample-imagenet-images.zip"
local_filepath = download(dummy_dataset_url, dummy_dataset_url.split("/")[-1])
with ZipFile(local_filepath, "r") as zipper:
zipper.extractall(".")
from PIL import Image
import os
import numpy as np
dataset_path = "sample-imagenet-images"
image_paths = sorted([os.path.join(dataset_path, x) for x in os.listdir(dataset_path)])
real_images = [np.array(Image.open(path).convert("RGB")) for path in image_paths]
这些是 ImageNet-1k 类别中的 10 张图像:“cassette_player”、“chain_saw”(x2)、“church”、“gas_pump”(x3)、“parachute”(x2)和“tench”。
真实图像。
图像加载完毕,让我们对它们进行一些轻量级预处理,以便用于 FID 计算。
from torchvision.transforms import functional as F
import torch
def preprocess_image(image):
image = torch.tensor(image).unsqueeze(0)
image = image.permute(0, 3, 1, 2) / 255.0
return F.center_crop(image, (256, 256))
real_images = torch.cat([preprocess_image(image) for image in real_images])
print(real_images.shape)
# torch.Size([10, 3, 256, 256])
我们现在加载`DiTPipeline`,以生成以上述类别为条件的图像。
from diffusers import DiTPipeline, DPMSolverMultistepScheduler
dit_pipeline = DiTPipeline.from_pretrained("facebook/DiT-XL-2-256", torch_dtype=torch.float16)
dit_pipeline.scheduler = DPMSolverMultistepScheduler.from_config(dit_pipeline.scheduler.config)
dit_pipeline = dit_pipeline.to("cuda")
seed = 0
generator = torch.manual_seed(seed)
words = [
"cassette player",
"chainsaw",
"chainsaw",
"church",
"gas pump",
"gas pump",
"gas pump",
"parachute",
"parachute",
"tench",
]
class_ids = dit_pipeline.get_label_ids(words)
output = dit_pipeline(class_labels=class_ids, generator=generator, output_type="np")
fake_images = output.images
fake_images = torch.tensor(fake_images)
fake_images = fake_images.permute(0, 3, 1, 2)
print(fake_images.shape)
# torch.Size([10, 3, 256, 256])
现在,我们可以使用`torchmetrics`计算 FID。
from torchmetrics.image.fid import FrechetInceptionDistance
fid = FrechetInceptionDistance(normalize=True)
fid.update(real_images, real=True)
fid.update(fake_images, real=False)
print(f"FID: {float(fid.compute())}")
# FID: 177.7147216796875
FID 越低越好。以下几点会影响 FID:
- 图像数量(真实图像和伪造图像)
- 扩散过程中引入的随机性
- 扩散过程中的推理步骤数
- 扩散过程中使用的调度器
对于最后两点,因此,最佳实践是在不同的种子和推理步骤下运行评估,然后报告平均结果。
FID 结果往往不稳定,因为它依赖于许多因素
- 计算中使用的特定 Inception 模型。
- 计算的实现精度。
- 图像格式(从 PNG 或 JPG 开始不同)。
考虑到这一点,FID 通常在比较相似的运行结果时最有用,但除非作者仔细披露 FID 测量代码,否则很难重现论文结果。
这些点也适用于其他相关指标,如 KID 和 IS。
作为最后一步,我们来目视检查 `fake_images`。
假图像。