📑 本页目录(点开跳转)
08 · ControlNet 与风格 LoRA
⏱ 22 分钟 | ⭐ 提示词管「画什么」,这一章补上「画成什么形状」和「用什么风格画」
🎯 一句话
文字说得清「一只猫在雪地里」,说不清「猫站在画面左三分之一、头偏右、镜头俯视」。 ControlNet 补形状,风格 LoRA 补风格。⭐ 它们和提示词三者互不替代 —— 这是这一章最容易搞混的地方。
🖐️ 一、提示词到底控制不了什么
上一章把接口钉死了:去噪网络吃 (x_t, t, c)。提示词只是 c 的一种,所以「加控制」只有两条路:往 c 里塞更强的东西,或者改那个函数本身。下面两个主角各走一条。
| 你想控制的 | 文字够不够 |
|---|---|
| 画什么(一只猫、雪地、黄昏) | ✅ 够,这正是文本编码器擅长的语义 |
| 画成什么形状(构图、姿势、线稿) | ❌ 说不清 —— 每加一条空间约束,全部命中的概率就掉一截,你还无法验证它听了哪几条 |
| 用什么风格画(某种笔触、你的角色) | ❌ 说了也没用 —— 不是描述得更细就行,是底座根本没见过它 |
🧬 二、ControlNet:复制一份主干,再用零卷积接回去
- 冻结原来的去噪主干(上一章那个 U-Net),一个权重都不动。
- 把它编码器那一半复制一份,这份可训练 —— 起点是会画画的权重,不是随机数。
- 条件图(Canny 边缘图 / 深度图 / 姿态骨架)经一个小编码器,加到副本输入上。
- 副本每一级的输出,各经一个零初始化的 1×1 卷积,加进冻结主干解码器同级。
第 4 步那个「零初始化」是整个设计的支点:
# 零初始化卷积:训练前输出恒为 0,梯度却不为 0 —— 所以它接得进去,也学得起来
import torch
import torch.nn as nn
torch.manual_seed(0)
frozen = torch.randn(1, 320, 8, 8) # 冻结主干这一级的输出
branch = torch.randn(1, 320, 8, 8) # 可训练副本这一级的输出
zc = nn.Conv2d(320, 320, 1) # ⭐ ControlNet 接回主干用的 1×1 卷积
nn.init.zeros_(zc.weight); nn.init.zeros_(zc.bias)
out = frozen + zc(branch) # ControlNet 就是这么接回去的
print("接回后的最大差", out.sub(frozen).abs().max().item())
out.sum().backward()
print("权重梯度范数 ", round(zc.weight.grad.norm().item(), 1))
print("默认初始化的话", round(nn.Conv2d(320, 320, 1)(branch).abs().max().item(), 3))
接回后的最大差 0.0
权重梯度范数 2501.0
默认初始化的话 2.217
0.0 说明第 0 步加了 ControlNet 的模型和原模型逐位相同;但梯度 2501 不是 0(里面带着副本这一级输出了什么),所以它从第一步就开始长。⚠️ 换成默认初始化,训练还没开始就往训好的模型里灌进 2.217 的随机改动。
⭐ 零初始化把「加一条新支路」从破坏性改动变成了从零长出来的增量。 这也是它只用几万张图就训得起来的原因 —— 不必重新学画画,只需学「怎么把条件图掺进去」。
💡 上一章 DiT 的 adaLN-Zero 门控也是零初始化,同一个想法。
🎨 三、风格 LoRA:和语言模型那个是同一个东西
不是「像」,是同一个低秩适配:底座 W 冻结,旁边挂一对窄矩阵,前向变成 W·x + B(A·x),中间那个窄维度叫 rank(记作 r)。⭐ B 零初始化,所以挂上那一刻 W + BA = W —— 和上一节的零卷积同一个动机。
拿交叉注意力里 768 → 320 那层算笔账:原层 768×320 = 245760 个参数,LoRA 只有 r×(768+320) = 1088r 个。
r |
可训练参数 | 占原层 |
|---|---|---|
| 4 | 4352 | 1.77% |
| 64 | 69632 | 28.33% |
⚠️ 所以 r 不是越大越好:到 64 已经离全量微调不远,小数据下更容易把训练集背下来。常见取值就在 4~64,主要挂在交叉注意力的 Q/K/V 和输出投影上 —— 那正是文本条件落到图像特征的地方。
为什么风格只能走这条路:风格是分布层面的偏移(笔触走向、色调、留白),几个形容词定位不了,得拿 20~100 张风格一致的图去改权重。
⚖️ 四、三者的分工(这一章最该记住的一张表)
| 管什么 | 怎么进模型 | 代价 | |
|---|---|---|---|
| 提示词 | 画什么 —— 内容、语义 | 文本编码器 → 条件 c |
免费,改字就行 |
| ControlNet | 画成什么形状 —— 构图、姿势 | 一条并行支路,逐级加进主干 | 多跑一份编码器副本 |
| 风格 LoRA | 用什么风格画 —— 笔触、角色 | 直接改主干若干层的权重 | 算力几乎不加,但要训 |
- 只给 ControlNet 不给提示词 → 形状对了,它不知道该画猫还是狗。
- 只给 LoRA 不给 ControlNet → 风格稳了,构图还是每次都变。
- 提示词写「站在左边」只让概率偏一点;ControlNet 给的是逐像素的约束。
💸 五、代价
① ControlNet 要多跑一份主干。 副本是编码器那一半,推理时那部分跑两遍,参数和激活都多一份。⚠️ 更贵的是它和 CFG 相乘 —— CFG 本来就要跑两次去噪网络,加了 ControlNet 之后这两次都得带上副本。
② 多个 ControlNet 会打架。 条件互相矛盾时(线稿说手在这儿、深度图说手在那儿),出图就崩在那个位置。
③ 多个 LoRA 更麻烦:BA 直接加进同一个 W,⚠️ 它们没有任何机制知道对方存在,只能每个配一个系数靠试(diffusers 里就是 set_adapters([...], adapter_weights=[0.8, 0.6]) 那两个数)。风格 LoRA 和角色 LoRA 打架是最常见的一种。
④ 风格 LoRA 会越界。 训练集只有几十张时,它常连背景和构图一起学走,那就侵占了 ControlNet 的地盘。症状很好认:不管提示词怎么改,构图都像训练集那几张。解法是减 r、减步数,或让训练集构图更杂。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 大模型全景导论 主线 5 · 怎样把模型变成你的助手 | ⭐ 同一个低秩适配:那页讲原权重 W、全量微调要学的 ΔW、以及用两个更窄的矩阵 BA 去近似它,还多讲一件本章没讲的 —— 什么时候根本不该微调 |
| 机器学习与深度学习基础 12 · CNN 处理图像 | 「零卷积」就是它「三个变体」那节的 1×1 卷积:不看空间、只在通道间做线性组合。⭐ 接回主干那个 +,也是它残差连接那节的同一个 + |
| AI基础设施 09 · 显存优化全家桶 | 训 ControlNet 多出来的副本让参数、梯度、优化器状态三块一起翻。那章第一节的判断很实用:把 batch 调到 1 还 OOM 吗 —— 还 OOM 是这三块的问题,不 OOM 才是激活的 |
| 大模型全景导论 10b · 图像生成 | 14 分钟的索引版,这两样在那里只占组件表一行。⭐ 而它的「📦 深挖的话要学什么」把 ControlNet 和风格 LoRA 训练标成 📕 整块要外找 —— 本章就是那一行 |
✅ 检查点
- 提示词说不清「形状」和说不清「风格」,失败方式有什么不同?
- ControlNet 冻结哪一份、训练哪一份?副本复制主干的哪一半,又加回哪一半?
- 零初始化的卷积第 0 步输出是多少?那它靠什么学起来?换成默认初始化会怎样?
r=64时 LoRA 的可训练参数占原层多少?为什么它不是越大越好?- 为什么 ControlNet 的算力代价会和 CFG「相乘」?多个 LoRA 叠加时又要调什么、为什么?
👀 答案
- 形状是说不清(文字表达不了逐像素的空间约束);风格是说了也没用(底座根本没见过那个角色或笔触)。
- 原主干全部冻结,复制出来的编码器那一半可训练;副本每级输出经零卷积加进冻结主干的解码器同级。
- 恒为 0,所以模型和原模型逐位相同(
0.0)。但权重梯度是 2501.0 不是 0,里面带着副本这一级的输出,所以它从第一步就开始长。默认初始化则在第 0 步就把主干改动 2.217,等于往训好的模型里灌噪声。 - 28.33%(
r=4只有 1.77%)。r一大就接近全量微调,小数据下更容易背下训练集,常见 4~64。 - CFG 本身要跑两次去噪网络,加了 ControlNet 后这两次都得带上编码器副本,所以相乘不是相加。多个 LoRA 要调各自的权重系数(如 0.8 / 0.6),因为它们的
BA直接加进同一个W,没有任何机制知道对方存在。
🛑 可以停在这里
读到这里你已经能看懂一份出图工作流:哪部分是提示词、哪部分是 ControlNet 的条件图、挂了几个 LoRA、那几个 0.x 在调什么。⭐ 更重要的是出了问题知道该动哪一个。
什么时候回来:加了 LoRA 之后构图被锁死时回看第五节 ④;显存或耗时突然翻倍时回看第五节 ①。
⚡ 走神救援
先记住这几件事
- ControlNet 用额外空间条件控制结构,LoRA 用低秩更新适配风格等需求。
- 零初始化连接让 ControlNet 起步时保持底座输出,同时仍能学习。
- 多个控制或适配器会叠加计算与相互影响,逐项加入并分别调整强度。
下一节 👉 09-怎么评估.md