8.2 文生图:Diffusion 与 LLM 结合 文生图(Text-to-Image, T2I)是多模态生成的代表任务——输入一段文字描述,模型生成对应的图像。Stable Diffusion、DALL-E、Midjourney、FLUX 等模型让「凭空画图」成为日常工具。本节梳理文生图的核心技术:Diffusion 模型 + LLM 文本编码器。 一、Diffusion 模型的核心思想 Diffusion(扩散)模型的思想来自热力学——物质从有序逐渐扩散为无序。在生成模型中,它表现为: 前向过程:把一张清晰图像逐步加噪声,直到变成纯噪声 反向过程:训练一个神经网络,从纯噪声逐步去噪,还原出清晰图像 训练好去噪网络后,生成图像就是「从随机噪声开始,逐步去噪」。
文生图(Text-to-Image, T2I)是多模态生成的代表任务——输入一段文字描述,模型生成对应的图像。Stable Diffusion、DALL-E、Midjourney、FLUX 等模型让「凭空画图」成为日常工具。本节梳理文生图的核心技术:Diffusion 模型 + LLM 文本编码器。
Diffusion(扩散)模型的思想来自热力学——物质从有序逐渐扩散为无序。在生成模型中,它表现为:
训练好去噪网络后,生成图像就是「从随机噪声开始,逐步去噪」。
前向过程是一个马尔可夫链——每一步给图像加少量高斯噪声:
其中 \beta_t 是预先设定的方差表(variance schedule),通常从 10^{-4} 线性增到 0.02。
一个重要性质:可以直接从 x_0 跳到任意 x_t,无需逐步计算:
其中 \bar{\alpha}_t = \prod_{s=1}^{t}(1-\beta_s)。
工程意义:训练时可以直接采样任意 t 的加噪版本,不用一步步加。
反向过程是真正的学习目标——训练一个网络 \epsilon_\theta 预测每一步加入的噪声:
DDPM(Denoising Diffusion Probabilistic Models)证明了,训练目标可以简化为:
其中:
直觉:给模型一张噪声图与时间步,让它预测加的是什么噪声。预测对了,反向去噪就能还原图像。
\epsilon_\theta 网络的实现——U-Net 是 Diffusion 模型的主力。它的结构:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 360" font-family="sans-serif" font-size="12"> <text x="400" y="25" text-anchor="middle" font-weight="bold" font-size="14">U-Net 架构(用于 Diffusion 去噪)</text> <!-- 输入 --> <rect x="20" y="150" width="80" height="60" fill="#e1f5ff" stroke="#333" rx="6"/> <text x="60" y="180" text-anchor="middle">噪声图 x_t</text> <text x="60" y="200" text-anchor="middle">+ 时间步 t</text> <text x="60" y="220" text-anchor="middle">+ 文本条件</text> <!-- Encoder --> <g transform="translate(130,100)"> <rect x="0" y="0" width="60" height="160" fill="#fff4e1" stroke="#333" rx="4"/> <text x="30" y="80" text-anchor="middle" transform="rotate(-90 30 80)">Conv Block</text> <text x="30" y="180" text-anchor="middle">64×64</text> </g> <g transform="translate(210,120)"> <rect x="0" y="0" width="60" height="120" fill="#fff4e1" stroke="#333" rx="4"/> <text x="30" y="60" text-anchor="middle" transform="rotate(-90 30 60)">Conv Block</text> <text x="30" y="140" text-anchor="middle">32×32</text> </g> <g transform="translate(290,140)"> <rect x="0" y="0" width="60" height="80" fill="#fff4e1" stroke="#333" rx="4"/> <text x="30" y="40" text-anchor="middle" transform="rotate(-90 30 40)">Conv Block</text> <text x="30" y="100" text-anchor="middle">16×16</text> </g> <!-- Bottleneck --> <g transform="translate(370,160)"> <rect x="0" y="0" width="60" height="40" fill="#f5e1ff" stroke="#333" rx="4"/> <text x="30" y="25" text-anchor="middle">Bottleneck</text> <text x="30" y="60" text-anchor="middle">8×8</text> </g> <!-- Decoder (镜像) --> <g transform="translate(450,140)"> <rect x="0" y="0" width="60" height="80" fill="#e8f5e1" stroke="#333" rx="4"/> <text x="30" y="40" text-anchor="middle" transform="rotate(-90 30 40)">Up Block</text> <text x="30" y="100" text-anchor="middle">16×16</text> </g> <g transform="translate(530,120)"> <rect x="0" y="0" width="60" height="120" fill="#e8f5e1" stroke="#333" rx="4"/> <text x="30" y="60" text-anchor="middle" transform="rotate(-90 30 60)">Up Block</text> <text x="30" y="140" text-anchor="middle">32×32</text> </g> <g transform="translate(610,100)"> <rect x="0" y="0" width="60" height="160" fill="#e8f5e1" stroke="#333" rx="4"/> <text x="30" y="80" text-anchor="middle" transform="rotate(-90 30 80)">Up Block</text> <text x="30" y="180" text-anchor="middle">64×64</text> </g> <!-- 输出 --> <rect x="700" y="150" width="80" height="60" fill="#ffe1e1" stroke="#333" rx="6"/> <text x="740" y="180" text-anchor="middle">预测噪声 ε</text> <!-- 跨层连接 --> <line x1="190" y1="180" x2="610" y2="180" stroke="#999" stroke-dasharray="4,2"/> <line x1="270" y1="180" x2="530" y2="180" stroke="#999" stroke-dasharray="4,2"/> <line x1="350" y1="180" x2="450" y2="180" stroke="#999" stroke-dasharray="4,2"/> <text x="400" y="280" text-anchor="middle">虚线 = 跨层连接(skip connections)</text> <!-- 主线 --> <line x1="100" y1="180" x2="125" y2="180" stroke="#333" marker-end="url(#ar5)"/> <line x1="685" y1="180" x2="695" y2="180" stroke="#333" marker-end="url(#ar5)"/> <defs> <marker id="ar5" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"> <path d="M0,0 L8,4 L0,8 z" fill="#333"/> </marker> </defs> </svg>
U-Net 的特点:
模型的任务是预测噪声 \epsilon。
直接在像素空间做 Diffusion 计算量极大——一张 512×512 的图有 786432 个像素,每步去噪都要算这么大的张量。
Latent Diffusion(Stable Diffusion 的核心创新)的思路:先压缩到潜在空间,再在潜在空间做 Diffusion。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400" font-family="sans-serif" font-size="13"> <text x="450" y="25" text-anchor="middle" font-weight="bold" font-size="15">Stable Diffusion 架构</text> <!-- 文本输入 --> <rect x="20" y="80" width="100" height="60" fill="#e1f5ff" stroke="#333" rx="6"/> <text x="70" y="105" text-anchor="middle">文本 prompt</text> <text x="70" y="125" text-anchor="middle">"一只猫"</text> <!-- 文本编码器 --> <rect x="160" y="80" width="140" height="60" fill="#fff4e1" stroke="#333" rx="6"/> <text x="230" y="105" text-anchor="middle">CLIP 文本编码器</text> <text x="230" y="125" text-anchor="middle">→ 文本 embedding</text> <!-- 噪声 --> <rect x="20" y="180" width="100" height="60" fill="#999" stroke="#333" rx="6"/> <text x="70" y="205" text-anchor="middle" fill="#fff">随机噪声</text> <text x="70" y="225" text-anchor="middle" fill="#fff">(潜在空间)</text> <!-- U-Net --> <rect x="340" y="120" width="200" height="180" fill="#f5e1ff" stroke="#333" rx="8"/> <text x="440" y="145" text-anchor="middle" font-weight="bold">U-Net (去噪网络)</text> <text x="440" y="170" text-anchor="middle">输入: 潜在噪声 + 时间步</text> <text x="440" y="190" text-anchor="middle">+ 文本 embedding (Cross-Attn)</text> <text x="440" y="220" text-anchor="middle">逐步去噪 T 步</text> <text x="440" y="250" text-anchor="middle">输出: 去噪后的潜在表示</text> <!-- VAE 解码 --> <rect x="600" y="180" width="140" height="60" fill="#e8f5e1" stroke="#333" rx="6"/> <text x="670" y="205" text-anchor="middle">VAE Decoder</text> <text x="670" y="225" text-anchor="middle">潜在 → 像素</text> <!-- 输出图像 --> <rect x="780" y="180" width="100" height="60" fill="#ffe1e1" stroke="#333" rx="6"/> <text x="830" y="215" text-anchor="middle">生成图像</text> <!-- 箭头 --> <line x1="120" y1="110" x2="155" y2="110" stroke="#333" marker-end="url(#ar6)"/> <line x1="300" y1="110" x2="335" y2="160" stroke="#333" marker-end="url(#ar6)"/> <line x1="120" y1="210" x2="335" y2="210" stroke="#333" marker-end="url(#ar6)"/> <line x1="540" y1="210" x2="595" y2="210" stroke="#333" marker-end="url(#ar6)"/> <line x1="740" y1="210" x2="775" y2="210" stroke="#333" marker-end="url(#ar6)"/> <!-- 三个核心组件标注 --> <text x="100" y="340" text-anchor="middle">① 文本理解</text> <text x="440" y="340" text-anchor="middle">② 迭代去噪</text> <text x="670" y="340" text-anchor="middle">③ 解码到像素</text> <defs> <marker id="ar6" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"> <path d="M0,0 L8,4 L0,8 z" fill="#333"/> </marker> </defs> </svg>
VAE 由两部分组成:
VAE 预训练好后冻结,不在 Diffusion 训练中更新。
在潜在空间做 Diffusion:
把文本 prompt 编码为向量,作为 U-Net 的条件:
U-Net 通过 Cross-Attention 接收这个文本条件,引导去噪方向。
Latent Diffusion 是 Stable Diffusion 能在消费级 GPU 上运行的关键。
Diffusion 模型早期有个问题:生成结果与文本条件关联度不够强。模型可能生成与 prompt 弱相关的图像。
Classifier-Free Guidance(CFG) 是解决方案:
训练时让 U-Net 同时学习条件去噪与无条件去噪:
条件去噪: ε_θ(x_t, t, c) ← 给定文本 c 无条件去噪: ε_θ(x_t, t, ∅) ← 不给文本(用空 embedding)
推理时把两者外推:
其中 w 是引导强度(guidance scale):
典型值 w = 7.5,平衡相关性与质量。
CFG 让生成结果显著更贴近 prompt:
prompt: "a cute orange cat sitting on a windowsill" w=1: 生成一只猫,但可能不是橘色、不在窗台 w=7: 生成一只明确的橘色猫,明确在窗台上 w=15: 过度强化,可能画面失真
CFG 已成为文生图的标准配置。
用户的 prompt 通常简短模糊:
"画一只猫" ← 模糊,无数种可能
解决:
"画一只橘色的猫坐在红色沙发上" ← 颜色、姿态、家具都要准
解决:
文生图模型长期不会画文字(招牌、海报上的字):
"画一个写着 HELLO 的红色招牌" ← 早期模型生成乱码
SD3、FLUX 等新一代模型用 Diffusion Transformer 显著改善了文字渲染。
Diffusion 需要 T 步去噪(通常 20-50 步),生成慢。加速方法:
文生图已经渗透到多个行业:
广告、海报、插画、概念设计
商品图生成、虚拟模特、场景合成
游戏素材、概念图、纹理
教学插图、可视化
头像、表情包、艺术创作
文生图也带来伦理挑战:
这些问题正在被社会与法律逐步规范。
文生图的核心是 Diffusion 模型——通过前向加噪、反向去噪学习生成。Latent Diffusion(Stable Diffusion)通过 VAE 压缩到潜在空间,大幅降低计算成本。CLIP 文本编码器 + U-Net + VAE 是经典三件套。CFG 让生成更贴近 prompt。SD3、FLUX 等新一代模型用 DiT 提升质量。文生图已广泛应用于创意、电商、游戏等行业,但也带来版权与伦理挑战。
下一节(8.3)我们看更挑战的文生视频——特别是 Sora 的 DiT 架构如何生成高质量长视频。