prompt导航sd
- 前言:从“许愿”到“驾驶”——Prompt的真实身份
- 第一章:回顾“三巨头”:VAE, U-Net, 与我们今天的“主角”Text Encoder
- 第二章:Prompt的“
- 第三章:用代码亲手“解剖”CLIP Text Encoder
- case_3_1_dissect_text_encoder.py
- 第四章:“注入灵魂”—— Cross-Attention如何使用Prompt Embedding
- “Negative Prompt”的秘密:不仅仅是“不画什么”,更是“远离什么”
- 总结与展望:你已掌握语义控制的“缰绳”
前言:从“许愿”到“驾驶”——Prompt的真实身份
当我们向Stable Diffusion输入一段Prompt时,感觉就像在向一个“魔法盒子”许愿。我们许下“一只宇航员骑马”的愿望,盒子就“吐”出一幅画。
但今天,我们要打破这个“魔法”的幻象,以一个**“工程师”的视角,来审视这个过程。
Prompt,并非一句“愿望”,而是一个精密的“驾驶指令”。 它在模型内部,会被转化成一个极其丰富的、高维的“导航地图”**(Prompt Embedding),而U-Net这个“绘画引擎”,则会在去噪的每一步,都严格参照这份地图,来修正自己的方向。
今天,我们将一起坐上这艘“创世飞船”的驾驶舱,亲眼见证,我们的文字指令,是如何变成这份“导航地图”,并最终引导我们抵达“创意彼岸”的。
第一章:回顾“三巨头”:VAE, U-Net, 与我们今天的“主角”Text Encoder
在我们之前的章节中,已经了解了Latent Diffusion的宏观架构,它由三个核心部分组成:
VAE: 负责**“空间切换”**,在像素空间和潜在空间之间搭建“任意门”。
U-Net: 负责在**“潜在空间”中,执行核心的“去噪”**工作。
Text Encoder (通常是CLIP): 负责**“理解”我们的文字Prompt,并为U-Net提供“导航”**。
今天,我们的所有焦点,都将集中在第三个组件上。我们将把它彻底“拆开”,看看它内部究竟是如何运作的。
第二章:Prompt的“
”—— 从一维文本到三维“指令魔方”
Stable Diffusion v1.5的Text Encoder,规定了它能接收的文本最大长度是77个Token,并且它会将每个Token,都转换成一个768维的向量。让我们看看这个过程。
2.1 第一站:Tokenizer -> [1, 77] 的ID序列
你的Prompt,比如"a cat",首先被CLIP的Tokenizer处理。
分词: “a cat” -> [‘<|startoftext|>’, ‘a’, ‘cat’, ‘<|endoftext|>’]。它会自动在前后加上起始和结束标记。
查表: 将这些Token转换为ID,比如 [49406, 320, 524, 49407]。
填充(Padding): 为了让所有Prompt的长度都统一为77,它会在后面填充77 - 4 = 73个特殊的[PAD]
Token ID(通常是49407)。
输出: 一个形状为 [1, 77] 的整数Tensor。
2.2 第二站:CLIP的Embedding层 -> [1, 77, 768] 的初始向量
这个[1, 77]的ID序列,被送入Text Encoder的第一层——Embedding层。
如我们之前所学,这个层会为77个ID中的每一个,都“查字典”查出其对应的768维词嵌入向量。同时,它还会加上位置编码(Positional Embedding)。
输出: 一个形状为 [1, 77, 768] 的浮点数Tensor。此时,它已经包含了**“是什么词”和“在什么位置”**这两个基础信息。
2.3 终点站:CLIP的Transformer层 -> [1, 77, 768] 的最终“语义指令”
上一步得到的初始向量,还不够“聪明”。它只包含了每个词孤立的信息。
接下来,这个[1, 77, 768]的Tensor,会流经CLIP Text Encoder内部的多个Transformer Block层(通常是12层)。
在这些层里,会发生我们熟悉的**自注意力(Self-Attention)**计算。每个Token的向量,都会与其他所有Token的向量进行充分的“信息交流”和“上下文融合”。
输出: 一个形状同样是 [1, 77, 768] 的浮点数Tensor。但此时,这个Tensor里的每一个768维向量,都已经不再是代表单个词了,而是蕴含了整个句子上下文的、高度浓缩的“语义指令”。这个最终的输出,就是我们所说的prompt_embeds。
第三章:用代码亲手“解剖”CLIP Text Encoder
我们将用diffusers库,加载Stable Diffusion的Text Encoder,并亲手将一个Prompt喂给它,验证我们上面描述的维度变化。
从StableDiffusionPipeline中,单独加载tokenizer和text_encoder。
定义一个Prompt。
手动调用tokenizer,并打印出input_ids的形状,验证[1, 77]。
将input_ids喂给text_encoder,打印出最终输出prompt_embeds的形状,验证[1, 77, 768]。
打印出prompt_embeds的一部分数值,让读者直观感受这个“语义指令”的样子。
追踪Prompt从“文字”到“语义指令”的诞生之旅
case_3_1_dissect_text_encoder.py
import torch
from transformers import CLIPTokenizer, CLIPTextModel
# --- 1. 定义模型ID和设备 ---
model_id = "runwayml/stable-diffusion-v1-5"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# --- 2. 加载“双子星”:Tokenizer 和 Text Encoder ---
# Tokenizer 负责“分词”和“查页码” (文本 -> ID)
print("正在加载 CLIP Tokenizer...")
tokenizer = CLIPTokenizer.from_pretrained(model_id, subfolder="tokenizer")
# Text Encoder 负责“理解”和“升维” (ID -> 语义向量)
print("正在加载 CLIP Text Encoder...")
text_encoder = CLIPTextModel.from_pretrained(model_id, subfolder="text_encoder").to(device)
print("加载完成!\n")
# --- 3. 准备我们的Prompt ---
prompt = "A photograph of an astronaut riding a horse."
print(f"--- 原始Prompt ---\n'{prompt}'\n")
# --- 4. 第一站:Tokenizer的“预处理” ---
# 我们手动调用tokenizer,看看它到底做了什么
# padding="max_length" 会自动填充到模型的最大长度 (77)
# truncation=True 如果句子太长,会自动截断
# return_tensors="pt" 返回PyTorch Tensor
inputs = tokenizer(
prompt,
padding="max_length",
truncation=True,
return_tensors="pt"
)
input_ids = inputs["input_ids"]
print(f"--- 经过Tokenizer处理后 ---")
print(f"Token IDs (部分): {input_ids[0, :10]}...") # 只显示前10个token
print(f"Token IDs 的形状: {input_ids.shape}")
print("✅ 验证成功:形状为 [1, 77],符合预期!")
print("-" * 50)
# --- 5. 终点站:Text Encoder的“深度加工” ---
# 将Token ID送入Text Encoder
# 我们不需要计算梯度,所以使用torch.no_grad()
with torch.no_grad():
outputs = text_encoder(input_ids.to(device))
prompt_embeds = outputs.last_hidden_state
print(f"--- 经过Text Encoder处理后 ---")
print(f"最终输出 Prompt Embeddings 的形状: {prompt_embeds.shape}")
print("✅ 验证成功:形状为 [1, 77, 768],符合预期!")
# --- 6. 深入观察“语义指令” ---
print("\n--- 让我们看看这个“语义指令”张量里面是什么 ---")
print(f"张量存放的设备: {prompt_embeds.device}")
print("这是第一个Token(<|startoftext|>)对应的768维语义向量 (只显示前5个值):")
print(prompt_embeds[0, 0, :5])
print("\n这是第五个Token('astronaut')对应的768维语义向量 (只显示前5个值):")
print(prompt_embeds[0, 4, :5])
print("\n🎉 成功!这个 [1, 77, 768] 的张量,就是指挥U-Net绘画的最终“导航地图”!")
运行这段代码,你会清晰地看到我们理论章节中描述的“升维之旅”的全过程:
从文本到ID序列:tokenizer 将我们的一句话,完美地转换成了一个长度为77的整数序列,并打包成 [1, 77] 的Tensor。
从ID序列到语义指令:text_encoder 接收这个ID序列,内部经过Embedding层和12层Transformer Block的复杂计算后,最终输出一个 [1, 77, 768] 的浮点数Tensor。
最关键的理解:输出的 prompt_embeds 中,每一个 768 维的向量(比如第5个向量,对应astronaut),都已经不再仅仅是它自己了。经过了多层自注意力机制的“信息融合”,它已经蕴含了整个句子的上下文信息。它知道自己是一个“宇航员”,并且是“正在骑马的宇航员”,而不是“在月球上行走的宇航员”。
第四章:“注入灵魂”—— Cross-Attention如何使用Prompt Embedding
现在,我们终于拿到了这份[1, 77, 768]的“导航地图”。U-Net是如何使用它的呢?答案是交叉注意力 (Cross-Attention)。
4.1 U-Net的“提问”:我是谁?我在哪?我该画什么?
在U-Net去噪的每一步,带噪的图像Latent,在U-Net内部也会被转换成一种向量表示。这个图像向
量,就像U-Net在“提问”:“根据我现在的样子(带噪图像),我下一步该怎么去噪,才能更像‘宇航员骑马’呢?”
在Cross-Attention中,这个来自图像的向量,扮演了Query (Q) 的角色。
4.2 Prompt Embedding的“回答”:K与V的语义供给
而我们精心准备好的prompt_embeds,则被用作Key (K) 和 Value (V)。
整个过程就像:
U-Net的图像Q,去和文本的K(prompt_embeds)进行匹配,计算“我当前图像的这个区域,和文本描述中的哪个词最相关?”。
得到注意力权重后,用它去加权求和文本的V(也是prompt_embeds)。
最终的结果,就是一个**“被文本语义浸染过”**的、新的图像特征向量。
这个过程,在U-Net的多个层级、多个步骤中反复发生。
4.3 【一张图看懂】Cross-Attention的“导航”机制
“Negative Prompt”的秘密:不仅仅是“不画什么”,更是“远离什么”
当你提供一个Negative Prompt时,Text Encoder会同样地将其编码成一个negative_prompt_embeds。
在去噪时,U-Net实际上会做两次预测:
有条件预测:使用你的Positive Prompt进行引导。
无条件预测:不使用任何Prompt(或者使用Negative Prompt)进行引导。
最终的去噪方向,是**“朝着Positive Prompt的方向走一步,同时再远离Negative Prompt/无条件的方向一小步”**。这就是为什么Negative Prompt能有效地移除不想要的元素,因为它在“语义地图”上,为生成过程提供了一个需要“避开”的区域。
总结与展望:你已掌握语义控制的“缰绳”
恭喜你!今天你已经深入到了Stable Diffusion的“神经中枢”,彻底理解了文字是如何化为一股无形的力量,去驾驭和雕刻图像的生成。
✨ 本章惊喜概括 ✨
你掌握了什么? | 对应的技能/工具 |
---|---|
追踪了Prompt的升维之旅 | ✅ 从文本 -> ID序列 [B, 77] -> 语义指令 [B, 77, 768] |
解剖了核心编码器 | ✅ CLIP Text Encoder的内部工作流程 |
洞悉了“导航”的秘密 | ✅ Cross-Attention如何将文本语义“注入”U-Net |
理解了负向提示 | ✅ Negative Prompt的“推开”原理 |
你不再仅仅是一个会“念咒语”的魔法师,你现在是一位懂得如何“绘制魔法阵”的符文大师。你已经掌握了AIGC领域中,最核心的语义控制的缰绳。 | |
🔮 敬请期待! 在下一篇文章中,我们将继续深入Tokenizer和Positional Embedding的更多细节,进一步巩固我们对文本表示的底层理解。AI新纪元的“内功心法”修炼,将越来越精深! |