1、引言
1.1 潜在空间的概念
潜在空间(Latent Space)是在机器学习和深度学习中一个重要的概念,它指的是用于表示数据的一种低维空间。这个空间编码了数据中包含的所有有用信息的压缩表示,通常比原始数据空间的维数更低,从而使其更容易进行分析和理解。
1.1.1 潜在空间的定义与生成
潜在空间可以被视为数据的一种抽象表示,它捕捉了数据的内在结构和特征。这种空间可以通过各种方法生成,如主成分分析(PCA)和深度神经网络(DNN)等。PCA是一种线性降维方法,它通过寻找原始数据的线性组合来生成潜在空间。而DNN则是一种非线性降维方法,它通过学习数据中的非线性关系来生成潜在空间。
1.1.2 潜在空间的应用
潜在空间在多个领域有着广泛的应用。首先,它可以用于数据压缩,将数据压缩到更小的空间,从而节省存储空间和计算资源。其次,潜在空间也可以用于数据可视化,帮助人们理解数据中隐藏的模式和趋势。此外,在数据挖掘和机器学习中,潜在空间也发挥着重要作用,用于发现数据中隐藏的知识和提高模型的性能。
1.1.3 潜在空间的特性
潜在空间具有一些独特的特性。首先,它是低维的,这使得数据更易于处理和分析。其次,潜在空间中的点或向量通常代表原始数据空间中的不同实例或特征,因此可以通过操作这些点或向量来影响原始数据。最后,潜在空间中的结构和关系往往反映了原始数据的内在属性和规律。
潜在空间是机器学习和深度学习中一个重要的概念,它通过将数据压缩到低维空间来捕捉数据的内在结构和特征。潜在空间具有广泛的应用,包括数据压缩、数据可视化、数据挖掘和机器学习等。通过探索和利用潜在空间,我们可以更好地理解数据、发现数据中隐藏的知识并提高模型的性能。
1.2 潜在空间搜索
1.2.1 定义
潜在空间搜索(Latent Space Search)的概念主要围绕在深度学习和机器学习中对潜在空间(Latent Space)的利用和查询。潜在空间是机器学习中的一个低维空间,它编码了数据的潜在特征,用于简化数据特征的表达,以便发现某种规律模式。潜在空间搜索指的是在这个低维空间中寻找与特定查询或目标相关的点或区域的过程。
1.2.2 方法
潜在空间搜索可以采用多种方法,包括但不限于偏最小二乘(PLS)、潜在空间中的规则化匹配(RMLS)以及监督语义索引(SSI)等。这些方法通常涉及定义一个匹配函数,该函数将查询(如搜索查询或用户请求)和文档(或数据点)映射到潜在空间,并计算它们之间的相似度。
1.2.3 应用
潜在空间搜索在信息检索、推荐系统、图像搜索等领域有着广泛的应用。例如,在图像搜索中,可以使用潜在空间搜索来找到与查询图像在视觉上相似的其他图像。通过潜在空间搜索,我们能够快速准确地找到与查询相关的内容,提高搜索效率和用户体验。
1.2.3 技术细节
潜在空间搜索通常涉及将查询和文档映射到潜在空间的映射函数。这些映射函数可以是线性的(如PLS)或非线性的(如通过深度神经网络学习得到的)。在某些情况下,可能需要学习这些映射函数,以便它们能够最好地捕捉数据中的结构和关系。这通常涉及到优化目标函数,如基于点击数据的pointwise loss。
1.2.4 优势
潜在空间搜索能够处理高维数据,并通过将数据映射到低维空间来减少计算复杂度。它还能够捕捉数据中的非线性关系,并发现传统方法可能无法发现的模式。这些优势使得潜在空间搜索在处理复杂数据和发现隐藏模式方面更加有效。
1.2.5 挑战
潜在空间搜索可能面临非凸优化问题的挑战,需要采用合适的优化算法来找到全局最优解。此外,在某些情况下,可能需要大量的训练数据来学习有效的映射函数。这些挑战限制了潜在空间搜索的适用性和效果,需要进一步的研究和探索来解决。
潜在空间搜索是在深度学习和机器学习中用于在潜在空间中寻找与特定查询或目标相关的点或区域的过程。它采用了多种方法和技术,并在多个领域有着广泛的应用。通过潜在空间搜索,我们能够更有效地处理高维数据,并发现数据中的隐藏模式和结构。
1.3 Stable Diffusion简介
1.3.1 Stable Diffusion定义
Stable Diffusion是一种深度学习模型,特别用于生成高质量的图像。它属于生成对抗网络(GANs)的一种,但与其他GANs相比,Stable Diffusion特别强调生成过程中的稳定性和高分辨率输出。该模型由Stability AI公司开发并于2022年发布。
1.3.2 核心技术
- 去噪扩散过程:Stable Diffusion的核心是一个去噪扩散过程,能够逐步将随机噪声转化为具有清晰结构的图像。
- 模型架构:Stable Diffusion采用了先进的模型架构,特别是其最新的版本Stable Diffusion 3,采用了与Sora相同的DiT(Diffusion Transformer)架构,支持多主题提示,并改进了文字书写效果。
1.3.3 Stable Diffusion特点
- 高质量图像生成:Stable Diffusion能够生成非常高质量的图像,包括艺术作品、风景、人脸等,且分辨率可以非常高。最新的XL版本可以在1024*1024像素的级别上生成可控的图像。
- 稳定性:在生成图像时,Stable Diffusion减少了传统GANs中常见的不稳定性和模式崩塌问题。
- 速度和效率:该模型优化了计算资源的使用,可以在相对较短的时间内生成图像,提高了实际应用中的效率。
- 文本到图像的生成:用户可以输入文本描述,模型会根据这些描述生成相应的图像。
- 开源与可控性:Stable Diffusion的部分版本是开源的,用户可以根据需求自由使用和修改模型。同时,用户可以较精确地控制生成图像的风格和内容。
1.3.4 应用领域
Stable Diffusion在艺术创作、游戏开发、虚拟现实等领域具有广泛的应用潜力。用户可以利用该模型生成各种高质量的图像内容,以满足不同的创作和娱乐需求。
1.3.5 挑战与前景
虽然Stable Diffusion已经取得了显著的成果,但其使用也伴随着责任和伦理考量。例如,需要避免滥用该技术生成虚假信息或侵犯版权的内容。未来,随着技术的不断发展和完善,Stable Diffusion有望在更多领域展现出其巨大的潜力和价值。
2. Stable Diffusion潜在空间搜索的实现
2.1 技术概要
生成图像模型学习视觉世界的“潜在空间”:这是一个低维向量空间,其中每个点映射到一个图像。从流形上的这样一个点回到一个可显示图像的过程称为“解码”——在Stable Diffusion模型中,这一过程由“解码器”模型来处理。
这个图像的潜在空间是连续和可插值的,意味着:
- 在空间上轻微移动只会轻微改变相应的图像(连续性)。
- 对于空间上的任意两个点A和B(即任意两幅图像),可以通过一条路径从A移动到B,路径上的每个中间点也在流形上(也就是也是有效的图像)。这些中间点将被称为两个起始图像之间的“插值”。
Stable Diffusion不仅仅是一个图像模型,它也是一个自然语言模型。它有两个潜在空间:一个是由训练期间使用的编码器学习到的图像表示空间,另一个是提示潜在空间,这是通过预训练和训练时微调的组合学习到的。
“潜在空间探索”,是采样潜在空间中的一个点并逐步改变潜在表示的过程。它最常见的应用是生成动画,其中每个采样点被送入解码器,并存储为最终动画的一帧。对于高质量的潜在表示,这可以产生连贯的动画。这些动画可以提供潜在空间特征图的洞察,并最终带来训练过程的改进。
本文将展示如何利用KerasCV中的Stable Diffusion API来执行提示插值,以及通过Stable Diffusion的视觉潜在空间和文本编码器的潜在空间进行循环游走。
2.1.1 设置
首先,我们导入KerasCV并使用教程中讨论的优化加载Stable Diffusion模型。
!pip install keras-cv --upgrade --quiet
import keras_cv
import keras
import matplotlib.pyplot as plt
from keras import ops
import numpy as np
import math
from PIL import Image
# 启用混合精度
# (只有在您有较新的NVIDIA GPU时才这样做)
keras.mixed_precision.set_global_policy("mixed_float16")
# 实例化Stable Diffusion模型
model = keras_cv.models.StableDiffusion(jit_compile=True)
通过使用此模型检查点,您承认其使用受CreativeML Open RAIL-M许可条款的约束,详情请见 https://raw.githubusercontent.com/CompVis/stable-diffusion/main/LICENSE
2.2 在文本提示之间进行插值
在Stable Diffusion中,文本提示首先被编码成向量,并且该编码用来指导扩散过程。潜在编码向量的形状是77x768(非常大!),当我们给Stable Diffusion一个文本提示时,我们实际上是从潜在流形上的这样一个点生成图像。
为了探索这个流形的更多部分,我们可以在两个文本编码之间进行插值,并在这些插值点生成图像。
下面的代码演示了如何通过两个文本提示来生成它们在潜在空间中的编码,并在这些编码之间进行插值。首先,我们定义了两个文本提示:一个描述了一只金毛寻回犬在海滩上的水彩画,另一个描述了一碗水果的静物DSLR照片。然后,我们设置了一个插值步骤数,即5,这意味着我们希望在两个编码之间生成5个等距的点。
接下来,我们使用模型的encode_text()函数对这两个文本提示进行编码,将它们转换为潜在空间中的向量。为了确保编码结果具有正确的形状,我们使用了ops.squeeze()函数来去除任何单一维度。
一旦我们有了这两个编码,我们就可以使用ops.linspace()函数在它们之间进行线性插值。这个函数会生成一个数组,其中包含interpolation_steps个等距的点,每个点都位于原始的两个编码之间。
最后,为了了解潜在空间中的编码大小,我们打印了第一个编码(encoding_1)的形状。由于两个编码都是通过相同的函数生成的,并且没有改变其形状,所以第二个编码(encoding_2)的形状也将是相同的。这个形状通常表示了潜在空间中向量的维度。
prompt_1 = "一只金毛寻回犬在海滩上的水彩画"
prompt_2 = "一碗水果的静物DSLR照片"
interpolation_steps = 5
encoding_1 = ops.squeeze(model.encode_text(prompt_1))
encoding_2 = ops.squeeze(model.encode_text(prompt_2))
interpolated_encodings = ops.linspace(encoding_1, encoding_2, interpolation_steps)
# 显示潜在流形的大小
print(f"编码形状: {encoding_1.shape}")
Downloading data from https://github.com/openai/CLIP/blob/main/clip/bpe_simple_vocab_16e6.txt.gz?raw=true
1356917/1356917 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
Downloading data from https://huggingface.co/fchollet/stable-diffusion/resolve/main/kcv_encoder.h5
492466864/492466864 ━━━━━━━━━━━━━━━━━━━━ 7s 0us/step
Encoding shape: (77, 768)
一旦我们进行了编码插值,我们就可以生成每个点的图像。注意,为了保持结果图像之间的稳定性,我们保持图像之间的扩散噪声不变。
首先,设置一个随机数种子seed,以确保结果的可复现性。程序员使用这个种子生成了一组具有特定形状和维度的随机噪声noise。在这里,噪声的形状是(512 // 8, 512 // 8, 4),这可能对应于某个特定分辨率图像的降采样版本(通常扩散模型会在较低分辨率上工作,然后逐步上采样到目标分辨率),而4可能代表噪声的不同通道或时间步长。
接下来,调用模型的generate_image方法,并传递了之前计算得到的插值编码interpolated_encodings、批处理大小batch_size(设置为插值步骤数interpolation_steps),以及前面生成的随机噪声noise。
seed = 12345
noise = keras.random.normal((512 // 8, 512 // 8, 4), seed=seed)
images = model.generate_image(
interpolated_encodings,
batch_size=interpolation_steps,
diffusion_noise=noise,
)
Downloading data from https://huggingface.co/fchollet/stable-diffusion/resolve/main/kcv_diffusion_model.h5
3439090152/3439090152 ━━━━━━━━━━━━━━━━━━━━ 26s 0us/step
50/50 ━━━━━━━━━━━━━━━━━━━━ 173s 311ms/step
Downloading data from https://huggingface.co/fchollet/stable-diffusion/resolve/main/kcv_decoder.h5
198180272/198180272 ━━━━━━━━━━━━━━━━━━━━ 1s 0us/step
现在我们已经生成了一些插值图像,让我们来看一下它们!
本文的源代码将把图像序列导出为gif,以便它们可以容易地在时间上下文中查看。对于概念上第一张和最后一张图像不匹配的图像序列,我们将橡皮筋绑在gif上。
from IPython.display import Image as IImage
IImage("doggo-and-fruit-5.gif")
def export_as_gif(filename, images, frames_per_second=10, rubber_band=False):
if rubber_band:
images += images[2:-1][::-1]
images[0].save(
filename,
save_all=True,
append_images=images[1:],
duration=1000 // frames_per_second,
loop=0,
)
export_as_gif(
"doggo-and-fruit-5.gif",
[Image.fromarray(img) for img in images],
frames_per_second=2,
rubber_band=True,
)
结果可能会令人惊讶。通常,在提示之间进行插值会产生看起来连贯的图像,并且经常展示两个提示内容之间的渐进概念转变。这表明了一个高质量的表示空间,它紧密地反映了视觉世界的自然结构。
为了更好地可视化这一点,我们应该进行更细粒度的插值,使用数百个步骤。为了保持批量大小小(这样我们就不会耗尽GPU内存),这需要我们手动批量处理我们的插值编码。
首先,设置了插值步骤数interpolation_steps为150,这意味着我们想要在文本描述1(encoding_1)和文本描述2(encoding_2)之间生成150个等距的插值点。然后,设置了批处理大小batch_size为3,这意味着每次处理3个插值点来生成图像。
接下来,通过ops.linspace函数在encoding_1和encoding_2之间进行线性插值,生成了interpolated_encodings数组,该数组包含了150个等距的插值点。
由于我们设置了批处理大小,所以需要将interpolated_encodings数组分割成多个批次。使用ops.split函数,我们根据batches的数量将interpolated_encodings分割成了多个部分,每个部分包含3个插值点(除了最后一个批次可能少于3个)。
然后,我们初始化一个空列表images来存储生成的图像。通过一个循环,我们遍历每个批次,并使用model.generate_image方法根据该批次的插值编码生成图像。model.generate_image方法接收当前批次的插值编码、批处理大小(在这里是固定的3)、生成图像所需的步骤数(num_steps设置为25)、以及之前生成的随机噪声noise作为参数。
对于每个生成的图像,我们使用Image.fromarray(img)将其转换为PIL图像对象,并添加到images列表中。
最后,我们使用export_as_gif函数(这个函数不是Python标准库或常见图像处理库中的函数,可能是自定义的或来自某个特定库)将所有生成的图像导出为一个GIF动画文件,文件名为"doggo-and-fruit-150.gif"。rubber_band=True可能是一个参数,用于在生成GIF时应用某种平滑或过渡效果。
interpolation_steps = 150
batch_size = 3
batches = interpolation_steps // batch_size
interpolated_encodings = ops.linspace(encoding_1, encoding_2, interpolation_steps)
batched_encodings = ops.split(interpolated_encodings, batches)
images = []
for batch in range(batches):
images += [
Image.fromarray(img)
for img in model.generate_image(
batched_encodings[batch],
batch_size=batch_size,
num_steps=25,
diffusion_noise=noise,
)
]
export_as_gif("doggo-and-fruit-150.gif", images, rubber_band=True)
25/25 ━━━━━━━━━━━━━━━━━━━━━━ 77s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 211ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 215ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 203ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 211ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 215ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 203ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 211ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 209ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 203ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
生成的gif展示了两个提示之间更清晰、更连贯的变化。试试您自己的提示并进行实验!
我们甚至可以扩展这个概念到多于一个图像。例如,我们可以在四个提示之间进行插值:
prompt_1 = "海滩上的金毛寻回犬的水彩画"
prompt_2 = "一碗水果的静物DSLR照片"
prompt_3 = "星空下的埃菲尔铁塔"
prompt_4 = "摩天大楼的建筑草图"
interpolation_steps = 6
batch_size = 3
batches = (interpolation_steps**2) // batch_size
encoding_1 = ops.squeeze(model.encode_text(prompt_1))
encoding_2 = ops.squeeze(model.encode_text(prompt_2))
encoding_3 = ops.squeeze(model.encode_text(prompt_3))
encoding_4 = ops.squeeze(model.encode_text(prompt_4))
interpolated_encodings = ops.linspace(
ops.linspace(encoding_1, encoding_2, interpolation_steps),
ops.linspace(encoding_3, encoding_4, interpolation_steps),
interpolation_steps,
)
interpolated_encodings = ops.reshape(
interpolated_encodings, (interpolation_steps**2, 77, 768)
)
batched_encodings = ops.split(interpolated_encodings, batches)
images = []
for batch in range(batches):
images.append(
model.generate_image(
batched_encodings[batch],
batch_size=batch_size,
diffusion_noise=noise,
)
)
def plot_grid(images, path, grid_size, scale=2):
fig, axs = plt.subplots(
grid_size, grid_size, figsize=(grid_size * scale, grid_size * scale)
)
fig.tight_layout()
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis("off")
for ax in axs.flat:
ax.axis("off")
images = images.astype(int)
for i in range(min(grid_size * grid_size, len(images))):
ax = axs.flat[i]
ax.imshow(images[i].astype("uint8"))
ax.axis("off")
for i in range(len(images), grid_size * grid_size):
axs.flat[i].axis("off")
axs.flat[i].remove()
plt.savefig(
fname=path,
pad_inches=0,
bbox_inches="tight",
transparent=False,
dpi=60,
)
images = np.concatenate(images)
plot_grid(images, "4-way-interpolation.jpg", interpolation_steps)
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 209ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 204ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 209ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 205ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 208ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 205ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 210ms/step
我们还可以在插值时允许扩散噪声变化,方法是省略diffusion_noise
参数:
images = []
for batch in range(batches):
images.append(model.generate_image(batched_encodings[batch], batch_size=batch_size))
images = np.concatenate(images)
plot_grid(images, "4-way-interpolation-varying-noise.jpg", interpolation_steps)
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 215ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 13s 254ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 12s 235ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 12s 230ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 214ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 208ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 210ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 209ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 208ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 10s 205ms/step
# 50/50 ━━━━━━━━━━━━━━━━━━━━ 11s 213ms/step
2.3 围绕文本提示进行搜索
我们的下一个实验将从特定提示产生的点开始,在潜在空间周围搜索。
首先先,我们设定了行走的步数walk_steps为150,以及每次生成图像时模型处理的批次大小batch_size为3。由此计算出总共需要的批次数batches,它是通过walk_steps除以batch_size得到的。我们还设定了每一步的步长step_size为0.005。
接着,我们使用模型将文本提示“星夜风格的埃菲尔铁塔”编码为潜在空间中的向量encoding,并通过ops.squeeze()函数去除任何多余的维度。这个编码的形状是(77, 768)。
然后,我们创建了一个与编码相同形状的单位矩阵delta,并将其所有元素乘以步长step_size。这个delta将作为每次在潜在空间中“行走”的步长。
在for循环中,我们模拟了在潜在空间中的“行走”过程。对于每一步,我们都将encoding添加到walked_encodings列表中,并将encoding增加delta以进行下一步。注意,这个“行走”过程是在潜在空间中进行的,而不是在图像空间。
完成所有步数的“行走”后,我们使用ops.stack()函数将walked_encodings列表中的所有编码堆叠成一个数组。然后,我们使用ops.split()函数将这个数组分割成多个批次,每个批次包含batch_size个编码。
接下来,我们再次使用for循环遍历每个批次,并使用模型为每个批次的编码生成图像。生成的图像被转换为PIL的Image对象,并添加到images列表中。在生成图像时,我们指定了生成步骤数num_steps为25,并提供了预定义的噪声noise。
最后,我们使用一个假设的函数export_as_gif()将所有生成的图像导出为一个GIF动画文件eiffel-tower-starry-night.gif。这个函数可能还接受一个rubber_band参数,用于控制GIF动画的平滑度。
walk_steps = 150
batch_size = 3
batches = walk_steps // batch_size
step_size = 0.005
encoding = ops.squeeze(
model.encode_text("星夜风格的埃菲尔铁塔")
)
# 注意,文本编码的形状是(77, 768)。
delta = ops.ones_like(encoding) * step_size
walked_encodings = []
for step_index in range(walk_steps):
walked_encodings.append(encoding)
encoding += delta
walked_encodings = ops.stack(walked_encodings)
batched_encodings = ops.split(walked_encodings, batches)
images = []
for batch in range(batches):
images += [
Image.fromarray(img)
for img in model.generate_image(
batched_encodings[batch],
batch_size=batch_size,
num_steps=25,
diffusion_noise=noise,
)
]
export_as_gif("eiffel-tower-starry-night.gif", images, rubber_band=True)
25/25 ━━━━━━━━━━━━━━━━━━━━ 6s 228ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 209ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 218ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 215ms/step
可能不足为奇,搜索离编码器的潜在空间太远会产生看起来不连贯的图像。程序员可以通过设置自己的提示,并调整step_size
来增加或减少散步的幅度来亲自尝试。注意,当散步的幅度变大时,散步经常会进入产生极其嘈杂图像的区域。
2.4 单个提示的扩散噪声空间搜索
我们最后的实验是使用同一个提示,并探索扩散模型可以从该提示产生的形象多样性。我们通过控制用于种子扩散过程的噪声来做到这一点。
我们创建两个噪声分量,x
和y
,并从0到2π进行散步,将我们的x
分量的余弦和我们的y
分量的正弦相加以产生噪声。通过这种方法,我们的散步结束时到达与开始时相同的噪声输入,因此我们得到一个“可循环”的结果!
首先,定义了一个文本提示prompt,即“荷兰风车旁田野中的奶牛油画”,然后使用模型model的encode_text方法将其编码为潜在空间中的向量encoding,并使用ops.squeeze去除任何多余的维度。
接下来,设置了行走步数walk_steps为150,批次大小batch_size为3,并计算出所需的批次数batches。
为了生成不同的噪声,代码使用了两个随机噪声向量walk_noise_x和walk_noise_y,它们的形状与预定义的噪声noise相同,并使用了Keras的random.normal函数来生成。
然后,代码定义了两组比例因子walk_scale_x和walk_scale_y,它们通过余弦和正弦函数以及线性空间函数ops.linspace生成,这样可以在不同的步数中引入不同的噪声权重。
接着,通过ops.tensordot函数将比例因子与噪声向量相乘,得到两个新的噪声向量noise_x和noise_y。然后,将这两个噪声向量相加得到最终的噪声noise。
由于生成过程是分批进行的,因此将总的噪声noise分割为多个批次batched_noise,每个批次的大小由batch_size决定。
现在,开始生成图像。在循环中,对于每个批次,使用模型model的generate_image方法生成图像。这里将固定的encoding(即文本提示的编码)作为输入,同时传入当前的噪声批次batched_noise[batch]、批次大小batch_size、生成步骤数num_steps(设为25)以及其他可能的参数(如diffusion_noise)。生成的图像被转换为PIL的Image对象,并添加到images列表中。
最后,使用假设的函数export_as_gif将所有生成的图像导出为一个名为cows.gif的GIF动画文件。
prompt = "荷兰风车旁田野中的奶牛油画"
encoding = ops.squeeze(model.encode_text(prompt))
walk_steps = 150
batch_size = 3
batches = walk_steps // batch_size
walk_noise_x = keras.random.normal(noise.shape, dtype="float64")
walk_noise_y = keras.random.normal(noise.shape, dtype="float64")
walk_scale_x = ops.cos(ops.linspace(0, 2, walk_steps) * math.pi)
walk_scale_y = ops.sin(ops.linspace(0, 2, walk_steps) * math.pi)
noise_x = ops.tensordot(walk_scale_x, walk_noise_x, axes=0)
noise_y = ops.tensordot(walk_scale_y, walk_noise_y, axes=0)
noise = ops.add(noise_x, noise_y)
batched_noise = ops.split(noise, batches)
images = []
for batch in range(batches):
images += [
Image.fromarray(img)
for img in model.generate_image(
encoding,
batch_size=batch_size,
num_steps=25,
diffusion_noise=batched_noise[batch],
)
]
export_as_gif("cows.gif", images)
25/25 ━━━━━━━━━━━━━━━━━━━━ 35s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 215ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 209ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 209ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 213ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 218ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 211ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 210ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 217ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 204ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 208ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 214ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 207ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 215ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 212ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 209ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 216ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 205ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 206ms/step
25/25 ━━━━━━━━━━━━━━━━━━ 5s 214ms/step
程序员可以尝试使用自己的文本提示,并尝试不同的unconditional_guidance_scale
值!
3. 实验源代码
"""
代码是关于如何使用Stable Diffusion模型在潜在空间中进行文本提示插值和搜索的。
```shell
# 安装keras-cv库,升级到最新版本并保持安静(不显示安装过程)
pip install keras-cv --upgrade --quiet
"""
# 导入所需的库
import keras_cv
import keras
import matplotlib.pyplot as plt
from keras import ops
import numpy as np
import math
from PIL import Image
# 启用混合精度(仅适用于较新的NVIDIA GPU)
keras.mixed_precision.set_global_policy("mixed_float16")
# 实例化Stable Diffusion模型
model = keras_cv.models.StableDiffusion(jit_compile=True)
## 在文本提示之间进行插值
"""
在Stable Diffusion中,文本提示首先被编码成向量,并用于指导扩散过程。潜在编码向量的形状是77x768,给定Stable Diffusion一个文本提示时,我们实际上是从潜在流形上的这样一个点生成图像。
为了探索这个流形的更多部分,我们可以在两个文本编码之间进行插值,并在这些插值点生成图像:
"""
# 定义两个文本提示
prompt_1 = "海滩上的水彩画,画的是一只金毛寻回犬"
prompt_2 = "一碗水果的静物DSLR照片"
interpolation_steps = 5
# 对文本提示进行编码
encoding_1 = ops.squeeze(model.encode_text(prompt_1))
encoding_2 = ops.squeeze(model.encode_text(prompt_2))
# 在两个编码之间进行插值
interpolated_encodings = ops.linspace(encoding_1, encoding_2, interpolation_steps)
# 显示潜在流形的大小
print(f"编码形状: {encoding_1.shape}")
"""
一旦我们进行了编码插值,我们就可以生成每个点的图像。为了保持生成图像间的稳定性,我们保持图像间的扩散噪声不变。
"""
# 设置随机种子并生成扩散噪声
seed = 12345
noise = keras.random.normal((512 // 8, 512 // 8, 4), seed=seed)
# 生成插值编码的图像
images = model.generate_image(
interpolated_encodings,
batch_size=interpolation_steps,
diffusion_noise=noise,
)
"""
现在我们已经生成了一些插值图像,让我们来看看它们!
整个教程中,我们将把图像序列导出为gif,这样它们就可以在时间上下文中轻松查看。对于概念上第一张和最后一张图像不匹配的图像序列,我们将橡皮筋绑在gif上。
如果您在Colab中运行,您可以通过运行以下代码查看自己的GIF:
"""
from IPython.display import Image as IImage
IImage("doggo-and-fruit-5.gif")
"""
定义一个函数,将图像序列导出为GIF格式:
"""
def export_as_gif(filename, images, frames_per_second=10, rubber_band=False):
if rubber_band:
images += images[2:-1][::-1] # 如果需要,添加橡皮筋效果
images[0].save(
filename,
save_all=True,
append_images=images[1:],
duration=1000 // frames_per_second,
loop=0,
)
# 导出GIF
export_as_gif(
"doggo-and-fruit-5.gif",
[Image.fromarray(img) for img in images],
frames_per_second=2,
rubber_band=True,
)
"""
### 接下来是关于如何在潜在空间中进行“散步”的实验
#### 围绕文本提示的散步
我们的下一个实验将从特定提示产生的点开始,在潜在流形周围散步。
"""
walk_steps = 150 # 散步的步数
batch_size = 3 # 批量大小
batches = walk_steps // batch_size # 计算批次数量
step_size = 0.005 # 步长
# 编码文本提示 "星夜风格的埃菲尔铁塔"
encoding = ops.squeeze(model.encode_text("The Eiffel Tower in the style of starry night"))
delta = ops.ones_like(encoding) * step_size # 创建步进向量
# 存储散步过程中的编码
walked_encodings = []
for step_index in range(walk_steps):
walked_encodings.append(encoding)
encoding += delta
walked_encodings = ops.stack(walked_encodings) # 将编码堆叠成一个列表
batched_encodings = ops.split(walked_encodings, batches) # 将编码分批
# 生成图像
images = []
for batch in range(batches):
images += [
Image.fromarray(img)
for img in model.generate_image(
batched_encodings[batch],
batch_size=batch_size,
num_steps=25,
diffusion_noise=noise,
)
]
# 导出散步过程为GIF
export_as_gif("eiffel-tower-starry-night.gif", images, rubber_band=True)
"""
#### 在单个提示的扩散噪声空间中进行圆形散步
我们的最后一个实验是坚持使用一个提示,并探索扩散模型可以从该提示产生的形象多样性。我们通过控制用于种子扩散过程的噪声来实现这一点。
我们创建两个噪声分量,`x` 和 `y`,并从 0 到 2π 进行散步,将 `x` 分量的余弦和 `y` 分量的正弦相加以产生噪声。通过这种方法,我们的散步结束时到达与开始时相同的噪声输入,因此我们得到一个“可循环”的结果。
"""
prompt = "荷兰风车旁田野中的奶牛油画"
encoding = ops.squeeze(model.encode_text(prompt))
walk_steps = 150
batch_size = 3
batches = walk_steps // batch_size
# 创建噪声分量和散步比例
walk_noise_x = keras.random.normal(noise.shape, dtype="float64")
walk_noise_y = keras.random.normal(noise.shape, dtype="float64")
walk_scale_x = ops.cos(ops.linspace(0, 2, walk_steps) * math.pi)
walk_scale_y = ops.sin(ops.linspace(0, 2, walk_steps) * math.pi)
noise_x = ops.tensordot(walk_scale_x, walk_noise_x, axes=0)
noise_y = ops.tensordot(walk_scale_y, walk_noise_y, axes=0)
noise = ops.add(noise_x, noise_y)
batched_noise = ops.split(noise, batches)
# 生成图像
images = []
for batch in range(batches):
images += [
Image.fromarray(img)
for img in model.generate_image(
encoding,
batch_size=batch_size,
num_steps=25,
diffusion_noise=batched_noise[batch],
)
]
# 导出散步过程为GIF
export_as_gif("cows.gif", images)
"""
尝试使用您自己的提示,并尝试不同的 `unconditional_guidance_scale` 值!
结果可能会令人惊讶。通常,在提示之间进行插值会产生看起来连贯的图像,并且经常展示两个提示内容之间的渐进概念转变。这表明了一个高质量的表示空间,它紧密地反映了视觉世界的自然结构。
为了更好地可视化这一点,我们应该进行更细粒度的插值,使用数百个步骤。为了保持批量大小小(这样我们就不会耗尽GPU内存),这需要我们手动批量处理我们的插值编码。
接下来的部分代码展示了如何进行更细粒度的插值,创建四向插值,以及如何在潜在流形周围进行“散步”。最后,代码展示了如何通过控制扩散过程中使用的噪声来探索扩散模型从一个提示生成的图像多样性。
"""
4. 总结
本文关于Stable Diffusion模型使用于潜在空间搜索的讨论,详细地介绍了如何探索这个模型所学习的潜在空间。
Stable Diffusion是一个强大的生成模型,它能够学习视觉世界的潜在流形,这是一个低维向量空间,其中每个点都对应一个图像。在Stable Diffusion中,将一个潜在空间中的点解码回一个可显示的图像是由解码器模型处理的。这个潜在流形是连续和可插值的,意味着小的移动只会导致图像的小变化,并且任何两个点之间都可以通过流形上的路径进行插值。
Stable Diffusion不仅是一个图像模型,它还是一个自然语言模型,拥有两个潜在空间:一个由训练期间使用的编码器学习得到的图像表示空间,另一个是通过预训练和训练时微调学习到的提示潜在空间。在潜在空间中游走,即采样潜在空间中的点并逐步改变潜在表示,是生成动画的常见应用,这些动画可以提供潜在空间特征图的洞察,从而可能带来训练过程的改进。
本文使用KerasCV中的Stable Diffusion API,指导读者如何执行提示插值,以及如何在Stable Diffusion的视觉潜在流形和文本编码器的潜在流形中进行循环游走。
本文提供了详细的代码示例,包括如何安装KerasCV库、实例化Stable Diffusion模型、进行文本编码插值、生成图像,以及将图像序列导出为GIF动画。它还描述了几种实验,如在文本提示周围进行“散步”,以及通过控制扩散过程中使用的噪声来探索模型可以产生的形象多样性。
Stable Diffusion不仅仅是单一的文本到图像生成工具。探索文本编码器的潜在流形和扩散模型的噪声空间是两种有趣的方式,可以让程序员体验到这个模型的强大功能。