1. 核心组件加载
一个完整的 Stable Diffusion 潜在扩散模型主要由以下 5 个核心组件构成:
- VAE (Variational Autoencoder) : 一个图像自编码器,负责在像素空间 (我们看到的图片)和潜空间(UNet 处理的低维数据)之间进行转换。
- Tokenizer: 文本分词器,负责将人类语言(提示词)转换为模型能够理解的数字 ID(Tokens)。
- Text Encoder: 文本编码器(通常是 CLIP),负责将 Token ID 转换为包含丰富语义信息的词嵌入向量(Embeddings)。
- UNet: 模型的核心,一个在潜空间中运行的噪声预测模型。它在每一步根据文本编码器的指导,预测并移除噪声。
- Scheduler: 调度器,负责定义去噪的时间步长和策略。它根据 UNet 的预测结果,计算出下一个时间步的潜空间状态。
python
# 加载所有组件,并指定 subfolder
repo_id="CompVis/stable-diffusion-v1-4"
vae = AutoencoderKL.from_pretrained(repo_id, subfolder="vae", ...)
tokenizer = CLIPTokenizer.from_pretrained(repo_id, subfolder="tokenizer")
text_encoder = CLIPTextModel.from_pretrained(repo_id, subfolder="text_encoder", ...)
unet = UNet2DConditionModel.from_pretrained(repo_id, subfolder="unet", ...) # 注意:这里原代码有误,应使用 UNet2DConditionModel
scheduler = UniPCMultistepScheduler.from_pretrained(repo_id, subfolder="scheduler")
2. 提示词处理
这是将文本指令转换为 UNet "指导"信息的过程。
-
分词与编码 :将正面提示词(如"宇航员骑马")转换为词嵌入向量
text_emb
。 -
准备无条件/负面提示词 :为了实现 CFG,我们还需要一个"无指导"的基准。这通常是通过编码一个空字符串
""
来实现的,它代表了模型的"自由创作"状态,在应用上等同于一个负面提示词。这里只把提示词嵌入,还有负面提示词的嵌入,这里就不写负面提示词了,就用 "" -
拼接 :将无条件嵌入
uncond_embeddings
和有条件嵌入text_emb
拼接在一起,形成一个(2, 77, 768)
的张量,为 CFG 的并行计算做准备。
python
# 正面提示词
text_input = tokenizer(prompt, ...)
text_emb = text_encoder(text_input.input_ids.to(device))[0]
# 负面/无条件提示词 (空字符串)
uncond_input = tokenizer([""], ...)
uncond_embeddings = text_encoder(uncond_input.input_ids.to(device))[0]
# 拼接成一个批次为 2 的张量
text_embeddings = torch.cat([uncond_embeddings, text_emb])
3. 潜空间初始化
我们不直接生成图片,而是在潜空间中创建初始的随机噪声。
- 确定尺寸 :由于 VAE 的下采样因子为 8,一个
512x512
的图像对应的潜空间尺寸是64x64
。因此,初始噪声的尺寸是height // 8
和width // 8
。 - 调整噪声尺度 :使用
torch.randn
创建的是标准正态分布噪声(强度为 1)。但像UniPCMultistepScheduler
这样的高级调度器,其数学模型要求一个特定的初始噪声强度,这个值存储在scheduler.init_noise_sigma
中。因此,我们需要将标准噪声乘以这个值,进行缩放。
python
# 创建潜空间噪声
latents = torch.randn((batch_size, unet.config.in_channels, height // 8, width // 8), ...)
# 根据调度器要求进行缩放
latents = latents * scheduler.init_noise_sigma
4. 核心:去噪循环
这是从纯噪声生成图像潜空间表示的核心迭代过程。
python
scheduler.set_timesteps(num_inference_steps) # 使用正确的函数调用
for i, timestep in enumerate(scheduler.timesteps):
# ... 循环体 ...
循环中的每一步都包含以下关键操作:
-
复制 Latents: 这里只创建了正面提示词的随机噪声,负面提示词的直接复制一份正面的,对齐词向量嵌入
pythonlatent_model_input = torch.cat([latents] * 2)
-
UNet 预测 : 将复制后的
latents
和拼接好的text_embeddings
一同送入 UNet,UNet 会并行地预测出两个噪声:noise_uncond
(无指导的) 和noise_text
(有指导的)。 -
CFG 计算 : 应用公式
noise = noise_uncond + guidance_scale * (noise_text - noise_uncond)
,计算出最终的、被强力引导的噪声。 -
Scheduler 步进 : 调用
scheduler.step()
,根据最终的噪声,计算出下一个时间步的、噪声更少的latents
。
5. VAE 解码与保存
循环结束后,我们得到了最终的图像潜空间表示,需要将其解码成像素图像。
- 逆向缩放 : 在送入 VAE 解码器之前,需要用
1 / vae.config.scaling_factor
(即1 / 0.18215
) 对latents
进行一次逆向缩放,以匹配 VAE 解码器的输入尺度。 - 解码 : 调用
vae.decode(latents)
将潜空间表示转换回像素图像。 - 后处理与保存 : 将图像的数值范围从
[-1, 1]
转换到[0, 255]
,并保存为图片文件。