Stable Diffusion LoRA 微调:从 Dreambooth 到社区生态的完整指南

6319 字
32 分钟
Stable Diffusion LoRA 微调:从 Dreambooth 到社区生态的完整指南

1. SD 微调的核心问题#

1.1 为什么 Stable Diffusion 需要微调?#

预训练的 Stable Diffusion 是通用的图像生成模型——能画猫、画人、画风景、画建筑。但要让它学会特定概念(你的宠物、你的产品、你的艺术风格),就需要微调:

预训练 SD 能做的:
✓ "a cat on a couch"
✓ "a sunset over ocean"
✗ "my dog named Mochi" ← 需要微调
✗ "in the style of Ukiyo-e" ← 需要微调
✗ "my product on a white background" ← 需要微调

1.2 SD 微调的四种方法#

┌──────────────────────────────────────────────────────────────┐
│ SD 微调方法对比 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. DreamBooth (2023, Google) │
│ ────────────────────────────────────────────────────── │
│ 方法: 给定 3-5 张图像,微调整个 SD (UNet + Text Encoder) │
│ 目标: 让模型学会特定主体的完整表示 │
│ 效果: ★★★★★ (最强,捕捉主体细节) │
│ 成本: 高 (全量微调,显存需求大) │
│ │
│ 2. Textual Inversion (2022, Google) │
│ ────────────────────────────────────────────────────── │
│ 方法: 学习新的词嵌入向量代表新概念 │
│ 目标: 用新词触发新概念 │
│ 效果: ★★★☆☆ (弱,概念表示有限) │
│ 成本: 低 (只训练 embedding) │
│ │
│ 3. LoRA (Low-Rank Adaptation) │
│ ────────────────────────────────────────────────────── │
│ 方法: 在权重矩阵旁边添加低秩适配器 │
│ 目标: 高效学习新概念/风格 │
│ 效果: ★★★★☆ (好,效率和效果平衡) │
│ 成本: 中 (比 DreamBooth 低,比 TI 高) │
│ │
│ 4. LyCORIS (LoRA Beyond Correction) │
│ ────────────────────────────────────────────────────── │
│ 方法: LoRA 的扩展集合(含 LoCon、LoHA、DiP+ 等) │
│ 目标: 更精细的风格/概念控制 │
│ 效果: ★★★★☆ (灵活,支持更多变体) │
│ 成本: 中低 │
│ │
└──────────────────────────────────────────────────────────────┘

1.3 一句话概括 SD LoRA#

SD LoRA 微调的核心是”在不改变原始模型权重的情况下,通过低秩矩阵注入新知识”——DreamBooth 让模型学会”这只狗长什么样”,Textual Inversion 让模型学会”这个词代表什么概念”,而 LoRA 则是在权重空间中找到一个高效的方向来编码风格和概念,最终的成果就是 Civitai 上分享的一个个几 MB 到几百 MB 的 .safetensors 文件。

2. DreamBooth:主体级微调#

2.1 DreamBooth 的核心思想#

DreamBooth(DreamBooth: Fine Tuning Text-to-Image Diffusion Models for Subject-Driven Generation, Ruiz et al., 2023)是 Google 提出的主体级微调方法:

核心洞察:
- 预训练 SD 见过大量图像,概念表示是模糊的
- 微调后,模型可以为特定实例生成专属表示
- 需要一个唯一标识符来"指向"目标主体
关键设计:
- 引入特殊 token "V" 作为类标识符
- 目标: 微调后 "a V dog" 生成特定狗
- 使用重建损失 + KL 散度 + 主体保留损失

2.2 DreamBooth 的损失函数#

def dreambooth_loss():
"""
DreamBooth 的训练损失。
"""
return {
"扩散损失": {
"公式": "L_diff = E_{x,c,ε,t}[‖ε - ε_θ(α_t x + σ_t c, t)‖²]",
"说明": "标准 DDPM 噪声预测损失",
"作用": "让模型学会从噪声中重建图像",
},
"身份保留损失": {
"公式": "L_ID = E[‖x_recon - x_class‖²]",
"说明": "微调后的输出应保留主体特征",
"作用": "防止主体被过度改变",
},
"KL 散度": {
"公式": "L_KL = KL(θ_new || θ_pretrained)",
"说明": "防止新权重偏离预训练太远",
"作用": "保持模型的通用生成能力",
},
"总损失": {
"公式": "L_total = L_diff + λ_id * L_ID + λ_kl * L_KL",
"说明": "三损失加权求和",
"作用": "平衡生成质量与主体保真度",
},
}
def dreambooth_training_loop():
"""
DreamBooth 训练循环。
"""
def train(model, subject_images, class_images, optimizer, device):
"""
参数:
subject_images: 3-5 张目标主体的图像
class_images: 同类别的通用图像 (用于身份保留)
optimizer: AdamW
"""
for batch in dataloader:
# 1) 主体图像 + 特殊 prompt
subject_imgs = batch["subject"].to(device)
prompt = "a photo of a [V] dog" # [V] 是特殊 token
# 2) 加噪
t = sample_timesteps(len(subject_imgs))
noise = torch.randn_like(subject_imgs)
alpha_bar = diffusion_schedule[t]
noisy_imgs = alpha_bar.sqrt() * subject_imgs + (1 - alpha_bar).sqrt() * noise
# 3) 文本编码
context = text_encoder(prompt)
# 4) UNet 前向
noise_pred = unet(noisy_imgs, t, context)
# 5) 重建损失
loss_diff = F.mse_loss(noise_pred, noise)
# 6) 身份保留损失 (与类别图像的重建一致)
with torch.no_grad():
class_imgs = batch["class"].to(device)
class_noisy = add_noise(class_imgs, t)
class_pred = unet(class_noisy, t, context)
class_loss = F.mse_loss(class_pred, noise)
# 7) KL 散度
loss_kl = kl_divergence(model.weights(), pretrained_weights)
# 8) 总损失
loss = loss_diff + 0.1 * class_loss + 0.01 * loss_kl
# 9) 反向传播
loss.backward()
optimizer.step()
optimizer.zero_grad()
return loss

2.3 DreamBooth 的特殊 Token 设计#

DreamBooth 在文本编码器中添加新的词嵌入来表示目标主体:

def textual_inversion_embed():
"""
DreamBooth 的词嵌入设计。
"""
return {
"特殊 token": "[V]",
"含义": "代表目标主体的唯一标识符",
"使用方式": "a photo of a [V] dog",
"效果": "[V] dog 专门生成这个特定的狗",
"训练方式": "在 Text Encoder 的 embedding 层添加新向量",
"嵌入维度": "与 CLIP 文本编码器一致 (768d for ViT-L)",
"训练参数量": "~768K 参数 (仅 embedding)",
}

2.4 DreamBooth 的显存需求#

def dreambooth_vram():
"""
DreamBooth 微调的显存需求。
"""
return {
"SD 1.5 (FP16)": {
"模型权重": "~7GB",
"梯度": "~7GB (全量)",
"优化器": "~14GB",
"激活值": "~5GB",
"总计": "~33GB",
"实际需要": "24GB 显存 (如 RTX 3090)",
},
"SDXL (FP16)": {
"模型权重": "~30GB",
"梯度": "~30GB",
"优化器": "~60GB",
"激活值": "~20GB",
"总计": "~140GB",
"实际需要": "80GB 显存 (如 A100)",
},
"SD 1.5 + LoRA": {
"模型权重": "~7GB (冻结)",
"LoRA 梯度": "~0.1GB",
"优化器": "~0.2GB",
"激活值": "~5GB",
"总计": "~12GB",
"实际需要": "16GB 显存 (如 RTX 4060 Ti)",
},
}

3. Textual Inversion:词嵌入学习#

3.1 Textual Inversion 的核心思想#

Textual Inversion(An Image is Worth One Word: Inverting Text Embeddings, Gal et al., 2022)在词嵌入空间中学习一个新的向量来表示新概念:

核心洞察:
- CLIP 文本编码器把文本映射到向量空间
- 每个词对应一个 embedding 向量
- 如果能学习一个新向量 "S*" 代表目标概念
- 推理时只需输入 "a photo of S*" 即可触发新概念
优势:
- 不需要改变模型架构
- 只需要训练 embedding (768K 参数)
- 可以组合多个概念 embedding

3.2 Textual Inversion 的训练#

def textual_inversion_training():
"""
Textual Inversion 训练。
"""
def train(text_encoder, vae, unet, images, concept_token, optimizer, device):
"""
参数:
text_encoder: CLIP 文本编码器
images: 3-5 张目标图像
concept_token: 新概念的 token 名 (如 "SKS")
"""
# 1) 在 tokenizer 中添加新 token
tokenizer = text_encoder.tokenizer
num_added = tokenizer.add_tokens([concept_token])
text_encoder.resize_token_embeddings(len(tokenizer))
# 获取新 token 的 embedding
token_id = tokenizer.convert_tokens_to_ids(concept_token)
concept_embed = text_encoder.get_input_embeddings()
# 2) 训练循环
for img in tqdm(images):
# 图像 → 潜空间
with torch.no_grad():
z0 = vae.encode(img)
# 3) 加噪
t = sample_timesteps(1)
noise = torch.randn_like(z0)
alpha_bar = diffusion_schedule[t]
z_t = alpha_bar.sqrt() * z0 + (1 - alpha_bar).sqrt() * noise
# 4) 文本编码 (新 token 的 embedding)
# 新 token 的 embedding 可训练
prompt_embeds = concept_embed.weight[token_id] # 可训练
context = prompt_embeds.unsqueeze(0) # (1, D)
# 5) UNet 前向
noise_pred = unet(z_t, t, context)
# 6) 损失
loss = F.mse_loss(noise_pred, noise)
# 7) 反向传播 (只更新 embedding)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 8) 保存 embedding
saved_embed = concept_embed.weight[token_id].detach().cpu()
return saved_embed
return train

3.3 Textual Inversion vs DreamBooth#

def ti_vs_dreambooth():
"""
Textual Inversion vs DreamBooth 对比。
"""
return {
"训练参数": {
"Textual Inversion": "~768K (仅 embedding)",
"DreamBooth": "~860M (UNet) + ~123M (Text Encoder)",
},
"效果": {
"Textual Inversion": "弱,只能捕捉浅层概念",
"DreamBooth": "强,能捕捉主体的完整视觉特征",
},
"泛化能力": {
"Textual Inversion": "有限,可能过拟合训练图像",
"DreamBooth": "较好,能生成新姿态/背景的同主体",
},
"生成速度": {
"Textual Inversion": "快 (embedding 已训练)",
"DreamBooth": "快 (模型已微调)",
},
"组合能力": {
"Textual Inversion": "好 (embedding 可组合)",
"DreamBooth": "差 (模型权重直接叠加)",
},
"适用场景": {
"Textual Inversion": "简单风格、图标、logo",
"DreamBooth": "特定人物/宠物/产品",
},
}

4. LoRA for SD:低秩适配器#

4.1 LoRA 应用到 SD 的核心设计#

SD LoRA 的核心是在 UNet 和 Text Encoder 的权重矩阵旁边添加低秩适配器:

┌─────────────────────────────────────────────────────────────┐
│ SD LoRA 架构 │
│ │
│ 原始权重 (冻结 ❄️): │
│ W0 ∈ ℝ^{d×k} │
│ │
│ LoRA 适配器 (训练 🔥): │
│ A ∈ ℝ^{r×k} (初始化: 随机高斯) │
│ B ∈ ℝ^{d×r} (初始化: 零) │
│ │
│ 前向传播: │
│ h = W0 x + BAx │
│ 或: h = (W0 + BA)x │
│ │
│ 关键: │
│ - r << min(d, k) → 低秩 │
│ - 只更新 A 和 B,W0 冻结 │
│ - 最终权重 = W0 + BA │
│ │
└─────────────────────────────────────────────────────────────┘

4.2 SD 中的 LoRA 目标层#

SD 的 LoRA 需要选择性地应用到特定层:

def sd_lora_target_modules():
"""
SD LoRA 的目标层配置。
"""
return {
"SD 1.5 / SD 2.x": {
"UNet": {
"推荐目标": [
"to_q", "to_k", "to_v", "to_out", # Attention
"ff.net.0", "ff.net.2", # FFN
"conv1", "conv2", # 下/上采样块
],
"可选目标": [
"time_emb_proj", # 时间嵌入投影
"norm1", "norm2", # GroupNorm
],
},
"Text Encoder (CLIP)": {
"推荐目标": [
"q_proj", "k_proj", "v_proj", "out_proj", # Attention
],
"说明": "Text Encoder 的 LoRA 对风格影响更大",
},
"VAE": {
"推荐目标": ["一般不添加"],
"原因": "VAE 已足够好,微调可能导致重建质量下降",
},
},
"SDXL": {
"UNet": {
"目标层": "与 SD 1.5 相同",
"额外": "mid_block 添加更多层",
},
"Text Encoder 1 (OpenCLIP)": {
"推荐目标": ["q_proj", "v_proj"],
},
"Text Encoder 2 (CLIP)": {
"推荐目标": ["q_proj", "v_proj"],
},
"重要": "SDXL LoRA 推荐同时训练双文本编码器",
},
}
def lora_rank_selection():
"""
LoRA rank 选择指南。
"""
return {
"r=4 (极低)": {
"参数量": "~0.1M",
"适用": "简单风格、微弱效果",
"质量": "较差",
},
"r=8": {
"参数量": "~0.2M",
"适用": "快速实验",
"质量": "一般",
},
"r=16 (推荐)": {
"参数量": "~0.4M",
"适用": "大多数场景",
"质量": "良好",
},
"r=32": {
"参数量": "~0.8M",
"适用": "复杂风格、人物",
"质量": "很好",
},
"r=64": {
"参数量": "~1.6M",
"适用": "高质量要求",
"质量": "优秀",
},
"r=128+": {
"参数量": ">3M",
"适用": "接近全量微调效果",
"质量": "最佳",
"注意": "文件变大,组合困难",
},
}

4.3 SD LoRA 完整实现#

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, List, Optional
# ============ LoRA 层实现 ============
class LoRALinear(nn.Module):
"""
LoRA 线性层。
在原始线性层旁边添加低秩适配器。
"""
def __init__(self, in_features, out_features, rank=4, alpha=1.0):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
# 原始权重 (冻结)
self.weight = None # 将在 forward 时使用
# LoRA 适配器 (可训练)
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
def forward(self, x, weight):
"""
参数:
x: (B, *, in_features)
weight: 原始线性层的权重
"""
# 原始输出
base_out = F.linear(x, weight)
# LoRA 输出: h = Wx + (α/r) * BAx
lora_out = (self.lora_B @ self.lora_A) * self.scaling
return base_out + F.linear(x, lora_out)
class LoRAConv2d(nn.Module):
"""
LoRA 卷积层。
用于 SD UNet 的卷积层。
"""
def __init__(self, in_channels, out_channels, kernel_size=3,
stride=1, padding=1, rank=4, alpha=1.0):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
# 原始权重 (冻结)
self.weight = None
# LoRA 适配器
self.lora_A = nn.Parameter(torch.randn(rank, in_channels * kernel_size * kernel_size) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_channels * kernel_size * kernel_size, rank))
def forward(self, x, weight, stride=1, padding=1):
"""
参数:
x: (B, C, H, W)
weight: 原始卷积权重
"""
# 原始输出
base_out = F.conv2d(x, weight, stride=stride, padding=padding)
# LoRA 输出
lora_weight = (self.lora_B @ self.lora_A).view(
weight.shape[0], weight.shape[1], 3, 3
)
lora_out = F.conv2d(x, lora_weight * self.scaling, stride=stride, padding=padding)
return base_out + lora_out
# ============ SD LoRA 模型 ============
class SDLoRAModel(nn.Module):
"""
带 LoRA 的 Stable Diffusion 模型。
"""
def __init__(self, sd_model, rank=4, alpha=1.0, target_modules=None):
super().__init__()
self.sd_model = sd_model
self.rank = rank
self.alpha = alpha
# 冻结原始模型
for param in sd_model.parameters():
param.requires_grad_(False)
# 创建 LoRA 层
self.lora_layers = nn.ModuleDict()
self._inject_lora(target_modules or DEFAULT_SD_LORA_TARGETS)
def _inject_lora(self, targets):
"""
向目标层注入 LoRA。
"""
for name, module in self.sd_model.named_modules():
for target in targets:
if target in name and isinstance(module, (nn.Linear, nn.Conv2d)):
self._create_lora_layer(name, module)
def _create_lora_layer(self, name, module):
"""
为指定层创建 LoRA。
"""
if isinstance(module, nn.Linear):
lora = LoRALinear(
module.in_features, module.out_features,
rank=self.rank, alpha=self.alpha
)
elif isinstance(module, nn.Conv2d):
lora = LoRAConv2d(
module.in_channels, module.out_channels,
module.kernel_size, module.stride, module.padding,
rank=self.rank, alpha=self.alpha
)
self.lora_layers[name] = lora
def forward(self, x_t, t, context):
"""
前向传播。
LoRA 权重在 forward 中应用,不改变原始模型。
"""
# 遍历所有层,手动应用 LoRA
for name, module in self.sd_model.named_modules():
if name in self.lora_layers:
lora = self.lora_layers[name]
if isinstance(module, nn.Linear):
original_forward = module.forward
module.forward = lambda x, m=module, l=lora: \
l(x, m.weight)
elif isinstance(module, nn.Conv2d):
original_forward = module.forward
module.forward = lambda x, m=module, l=lora: \
l(x, m.weight, m.stride, m.padding)
# 标准 SD 前向
return self.sd_model(x_t, t, context)
def merge_weights(self):
"""
合并 LoRA 权重到原始模型。
合并后模型等价,但不再需要 LoRA 层。
"""
for name, module in self.sd_model.named_modules():
if name in self.lora_layers:
lora = self.lora_layers[name]
if isinstance(module, nn.Linear):
# W_new = W_old + (α/r) * B @ A
delta = (lora.lora_B @ lora.lora_A) * lora.scaling
module.weight.data = module.weight.data + delta
elif isinstance(module, nn.Conv2d):
delta = (lora.lora_B @ lora.lora_A).view(
module.weight.shape
) * lora.scaling
module.weight.data = module.weight.data + delta
def save_lora(self, path):
"""
保存 LoRA 权重。
"""
lora_state = {
name: {
"lora_A": layer.lora_A.data.cpu(),
"lora_B": layer.lora_B.data.cpu(),
"alpha": layer.alpha,
"rank": layer.rank,
}
for name, layer in self.lora_layers.items()
}
torch.save(lora_state, path)
@classmethod
def load_lora(cls, sd_model, path, rank=4, alpha=1.0):
"""
加载 LoRA 权重。
"""
lora_state = torch.load(path, map_location="cpu")
model = cls(sd_model, rank, alpha)
for name, state in lora_state.items():
if name in model.lora_layers:
model.lora_layers[name].lora_A.data = state["lora_A"]
model.lora_layers[name].lora_B.data = state["lora_B"]
return model

4.4 SD LoRA 训练循环#

def train_sd_lora(sd_model, vae, text_encoder, images, prompts,
rank=4, alpha=1.0, lr=1e-4, epochs=5):
"""
SD LoRA 训练循环。
"""
# 1) 创建 LoRA 模型
lora_model = SDLoRAModel(sd_model, rank=rank, alpha=alpha)
lora_model.train()
# 2) 只训练 LoRA 参数
optimizer = torch.optim.AdamW(
lora_model.lora_layers.parameters(),
lr=lr,
weight_decay=0.01,
)
# 3) 冻结 VAE 和 Text Encoder
vae.eval()
text_encoder.eval()
for epoch in range(epochs):
for img, prompt in tqdm(zip(images, prompts), total=len(images)):
# 图像 → 潜空间
with torch.no_grad():
z0 = vae.encode(img.unsqueeze(0)) # (1, 4, 64, 64)
# 采样时间步
t = torch.randint(0, 1000, (1,))
# 加噪
noise = torch.randn_like(z0)
alpha_bar = get_diffusion_schedule(t)
z_t = alpha_bar.sqrt() * z0 + (1 - alpha_bar).sqrt() * noise
# 文本编码
with torch.no_grad():
context = text_encoder(prompt) # (1, seq_len, 768)
# UNet 前向 (带 LoRA)
noise_pred = lora_model(z_t, t, context)
# 损失
loss = F.mse_loss(noise_pred, noise)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch {epoch + 1}: Loss = {loss.item():.4f}")
# 4) 保存 LoRA
lora_model.save_lora("my_style_lora.safetensors")
return lora_model
DEFAULT_SD_LORA_TARGETS = [
# UNet Attention
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn1.to_q",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn1.to_k",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn1.to_v",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn1.to_out.0",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn2.to_q",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn2.to_k",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn2.to_v",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.attn2.to_out.0",
# UNet FFN
"unet.up_blocks.0.attentions.0.transformer_blocks.0.ff.net.0",
"unet.up_blocks.0.attentions.0.transformer_blocks.0.ff.net.2",
# Text Encoder
"text_encoder.blocks.0.attn.q_proj",
"text_encoder.blocks.0.attn.k_proj",
"text_encoder.blocks.0.attn.v_proj",
]

5. LyCORIS:LoRA 变体集合#

5.1 LyCORIS 是什么?#

LyCORIS(LoRA Beyond Correction)是社区开发的 LoRA 扩展集合,包含多种 LoRA 变体:

LyCORIS 变体:
├── LoCon (LoRA on Convolution)
│ 在卷积层也添加 LoRA(SD LoRA 只在 attention 层)
├── LoHA (LoRA with Hadamard Product)
│ 用 Hadamard 积替代矩阵乘法: ΔW = H ⊙ (B @ A)
│ 效果: 更紧凑的表示,效果更好
├── LoKR (LoRA with Kronecker Product)
│ 用 Kronecker 积替代矩阵乘法
│ 效果: 保留更多结构信息
├── DiP (Decoupled LoRA Pre-trained)
│ 分解为方向 + 幅度两部分
├── GLora (Group LoRA)
│ 按组应用 LoRA,减少参数量
└── CompFAB (Comprehensive Fine-Adapter)
结合多种技术的复合适配器

5.2 LoCon:卷积层 LoRA#

class LoCon(nn.Module):
"""
LoCon: 在卷积层也应用 LoRA。
SD LoRA 默认只在 attention 层,LoCon 扩展到 conv 层。
"""
def __init__(self, in_channels, out_channels, kernel_size=3,
rank=4, alpha=1.0):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
# LoRA on weight (与 LoRA Linear 相同)
self.lora_A = nn.Parameter(torch.randn(rank, in_channels * kernel_size * kernel_size) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_channels * kernel_size * kernel_size, rank))
# LoRA on bias (可选)
if hasattr(nn.Conv2d, 'bias') and nn.Conv2d.bias is not None:
self.lora_bias = nn.Parameter(torch.zeros(out_channels))
else:
self.lora_bias = None
def forward(self, x, weight, bias=None):
# 原始输出
out = F.conv2d(x, weight, bias=bias, padding=1)
# LoRA 调整
delta = (self.lora_B @ self.lora_A).view(
weight.shape[0], weight.shape[1], 3, 3
) * self.scaling
out = out + F.conv2d(x, delta)
if self.lora_bias is not None:
out = out + self.lora_bias.view(1, -1, 1, 1)
return out

5.3 LoHA:Hadamard 积 LoRA#

class LoHA(nn.Module):
"""
LoHA: 使用 Hadamard 积的 LoRA。
ΔW = H ⊙ (B @ A)
其中 H 是可学习的 Hadamard 矩阵参数。
"""
def __init__(self, in_features, out_features, rank=4, alpha=1.0):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
# 低秩矩阵
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.randn(out_features, rank) * 0.01)
# Hadamard 矩阵 (对角矩阵,用向量表示)
self.h = nn.Parameter(torch.ones(out_features, rank))
def forward(self, x, weight):
# 原始输出
base_out = F.linear(x, weight)
# LoHA: ΔW = H ⊙ (B @ A)
# H 是 Hadamard (逐元素乘法)
delta = self.h * (self.lora_B @ self.lora_A)
lora_out = F.linear(x, delta * self.scaling)
return base_out + lora_out

5.4 LyCORIS 变体对比#

def lycoris_variants():
"""
LyCORIS 各变体对比。
"""
return {
"LoRA (标准)": {
"公式": "ΔW = B @ A",
"参数量": "2 × d × r",
"效果": "好",
"适用": "通用",
"代表": "标准 SD LoRA",
},
"LoCon": {
"公式": "LoRA + Conv LoRA",
"参数量": "比 LoRA 多 ~20%",
"效果": "更好(细节更丰富)",
"适用": "风格、纹理",
"代表": "社区广泛使用",
},
"LoHA": {
"公式": "ΔW = H ⊙ (B @ A)",
"参数量": "比 LoRA 多 ~50% (含 H)",
"效果": "更好(表示更紧凑)",
"适用": "风格、概念",
"代表": "Kohaku 系列",
},
"LoKR": {
"公式": "ΔW = Kronecker(B, K) @ A",
"参数量": "与 rank 成线性关系",
"效果": "保留结构",
"适用": "结构化变化",
"代表": "学术研究",
},
"DiP+": {
"公式": "ΔW = D ⊙ M",
"参数量": "d + r",
"效果": "效率高",
"适用": "轻量适配",
"代表": "轻量化需求",
},
}

6. SDXL LoRA:特殊考虑#

6.1 SDXL 的架构差异#

SDXL 相比 SD 1.5 有几个关键差异,需要在 LoRA 微调时特别注意:

def sdxl_differences():
"""
SDXL vs SD 1.5 的关键差异。
"""
return {
"双文本编码器": {
"SD 1.5": "1 个 CLIP ViT-L/14 (768d)",
"SDXL": "2 个编码器: OpenCLIP ViT-L/14 (768d) + OpenCLIP ViT-G/14 (1280d)",
"LoRA 影响": "需要同时在两个编码器上应用 LoRA",
},
"更大的 UNet": {
"SD 1.5": "~860M 参数",
"SDXL": "~3.5B 参数",
"LoRA 影响": "更大的微调空间,但也需要更高的 rank",
},
"更高的分辨率": {
"SD 1.5": "512×512 (latent 64×64)",
"SDXL": "1024×1024 (latent 128×128)",
"LoRA 影响": "高分辨率生成效果更好,但 LoRA 效果更明显",
},
"Refiner": {
"SD 1.5": "无",
"SDXL": "额外的 refiner 模型",
"LoRA 影响": "一般只训练 base UNet,refiner 可单独微调",
},
}
def sdxl_lora_targets():
"""
SDXL LoRA 推荐的 target modules。
"""
return {
"UNet (必需)": [
"to_q", "to_k", "to_v", "to_out", # 所有 attention 层
"ff.net.0", "ff.net.2", # FFN
"conv1", "conv2", # 卷积
"time_emb_proj", # 时间嵌入
],
"Text Encoder 1 (OpenCLIP ViT-L)": [
"q_proj", "k_proj", "v_proj", "out_proj",
],
"Text Encoder 2 (OpenCLIP ViT-G)": [
"q_proj", "k_proj", "v_proj", "out_proj",
],
"训练建议": {
"最少": "只训练 UNet (可获得 80% 效果)",
"推荐": "UNet + TE1 (最佳性价比)",
"完整": "UNet + TE1 + TE2 (最高质量)",
},
}

6.2 SDXL LoRA 训练配置#

def sdxl_lora_config():
"""
SDXL LoRA 训练推荐配置。
"""
return {
"rank": {
"UNet": "32-64 (SDXL UNet 更大,需要更高 rank)",
"TE1": "16-32",
"TE2": "8-16",
},
"alpha": {
"UNet": "64 (与 rank 相等或 2x)",
"TE": "rank (或 1.0)",
},
"学习率": {
"UNet": "1e-4",
"TE": "5e-5 (比 UNet 低)",
},
"batch_size": "1-4 (SDXL 显存需求更大)",
"resolution": "1024×1024 (原生)",
"epoch": "5-10",
"训练时间": "约 2-4 小时 (A100 80GB)",
}
def sdxl_vram():
"""
SDXL LoRA 显存需求。
"""
return {
"SDXL + LoRA r=32": {
"batch=1": "~24GB",
"batch=2": "~40GB",
"batch=4": "~70GB",
},
"优化技巧": {
"梯度检查点": "减少 ~30% 显存",
"CPU 卸载": "减少 ~40% 显存 (速度变慢)",
"混合精度": "FP16 训练,节省 ~50% 显存",
},
}

7. 社区生态与工具链#

7.1 Civitai:LoRA 分享平台#

Civitai 是最大的 SD 模型分享平台:

Civitai 生态:
├── 模型类型:
│ ├── Checkpoint (完整模型): ~2-7GB
│ ├── LoRA (适配器): ~100MB - 500MB
│ ├── Textual Inversion (embedding): ~100KB - 1MB
│ ├── ControlNet (条件控制): ~1-2GB
│ └── VAE (变分自编码器): ~300MB
├── 搜索和发现:
│ ├── 风格分类 (anime, realistic, art style)
│ ├── 主题分类 (character, clothing, pose)
│ ├── 评分和下载量
│ └── 社区讨论
└── 使用方式:
直接下载 .safetensors 文件
在 WebUI / ComfyUI 中加载
通过 API 访问 (civitai.com/api)

7.2 ComfyUI LoRA 工作流#

def comfyui_lora_workflow():
"""
ComfyUI 中加载 LoRA 的节点配置。
"""
return {
"节点顺序": [
"1. Load Checkpoint: 加载 SD 模型 (SD 1.5 / SDXL)",
"2. Load LoRA: 加载 .safetensors 文件,设置 strength",
"3. CLIP Text Encode: 输入正面提示词",
"4. CLIP Text Encode (neg): 输入负面提示词",
"5. KSampler: 设置采样参数",
"6. VAE Decode: 解码潜变量到图像",
],
"LoRA strength 参数": {
"strength_model": "对模型权重的影响 (1.0 = 完整应用)",
"strength_clip": "对 CLIP Text Encoder 的影响",
"常见设置": "0.5-0.8 (太强会失真)",
},
"多 LoRA 叠加": {
"方法": "多个 Load LoRA 节点串联",
"注意": "叠加太多会导致冲突和 artifacts",
"技巧": "先用低 strength 尝试叠加",
},
}

7.3 LoRA 的保存格式#

def lora_file_format():
"""
LoRA 文件格式 (.safetensors)。
"""
return {
"格式": "safetensors (安全的 pickle 替代)",
"内容": {
"lora_weight": "LoRA 权重矩阵 (A 和 B)",
"metadata": {
"base_model": "基础模型名称",
"rank": "LoRA rank",
"alpha": "alpha 值",
"train_steps": "训练步数",
"trained_by": "训练者",
},
},
"文件大小参考": {
"r=4": "~0.5MB",
"r=8": "~1MB",
"r=16": "~2-3MB",
"r=32": "~5-10MB",
"r=64": "~10-20MB",
"r=128": "~20-50MB",
},
"与 ckpt 的对比": {
"SD 1.5": "~4GB",
"SDXL": "~7GB",
"LoRA (r=32)": "~5-10MB (压缩率 ~400-1000x)",
},
}
def safetensors_save():
"""
保存为 safetensors 格式。
"""
from safetensors.torch import save_file
def save_lora_safetensors(lora_layers, path, metadata=None):
"""
保存 LoRA 为 safetensors 格式。
"""
tensors = {}
for name, layer in lora_layers.items():
tensors[f"{name}.lora_A"] = layer.lora_A.data.float()
tensors[f"{name}.lora_B"] = layer.lora_B.data.float()
tensors[f"{name}.alpha"] = torch.tensor(layer.alpha)
tensors[f"{name}.rank"] = torch.tensor(layer.rank)
save_file(tensors, path, metadata=metadata or {})
print(f"Saved LoRA to {path}")

8. 训练实践指南#

8.1 训练数据准备#

def training_data_preparation():
"""
SD LoRA 训练的数据准备。
"""
return {
"图像数量": {
"最低": "5-10 张 (可工作但不理想)",
"推荐": "20-50 张",
"最佳": "100-200 张",
"说明": "数量不是越多越好,关键是质量",
},
"图像要求": {
"分辨率": "至少 512×512,更高更好",
"多样性": "不同角度、不同背景、不同光照",
"一致性": "同一主体的不同变体",
"质量": "清晰、无水印、无文字",
"避免": "多人合影、复杂场景",
},
"标签/提示词": {
"正面": "描述主体的正面特征",
"负面": "一般用通用的负面词",
"技巧": "用 BLIP-2 自动生成描述",
},
"预处理": {
"质量过滤": "去除模糊、过曝、欠曝图像",
"主体裁剪": "将主体放在画面中心",
"背景简化": "白色或纯色背景更好",
"大小调整": "resize 到 512 或 1024",
},
}
def dataset_structure():
"""
推荐的数据集目录结构。
"""
return {
"root/": {
"images/": "所有训练图像 (jpg/png)",
"metadata.jsonl": "图像-描述对列表",
"config.toml": "训练配置",
},
"metadata.jsonl 示例": """
{"image": "images/img001.jpg", "prompt": "photo of sks dog in park"}
{"image": "images/img002.jpg", "prompt": "photo of sks dog sitting"}
{"image": "images/img003.jpg", "prompt": "photo of sks dog on couch"}
""",
"训练工具支持": {
"Kohya_ss": "支持文件夹 + jsonl",
"DirectML": "Windows 友好",
"lora-scripts": "自动化程度高",
},
}

8.2 Kohya_ss 训练配置#

def kohya_ss_config():
"""
Kohya_ss 训练 SD LoRA 的推荐配置。
"""
return {
"模型设置": {
"train_data_directory": "./train_data",
"reg_data_directory": "./reg_data", # 正则化数据 (可选)
"network_module": "networks.lora",
"network_dim": 32, # rank
"network_alpha": 32, # alpha
"clip_skip": 2, # 跳过 CLIP 最后 N 层
},
"训练设置": {
"batch_size": 4,
"resolution": "512,512",
"max_train_steps": 500-2000,
"learning_rate": "1e-4",
"lr_scheduler": "cosine_with_restarts",
"optimizer": "AdamW8bit", # 节省显存
},
"LoRA 目标": {
"unet": True,
"text_encoder": True, # 推荐开启
"text_encoder_lr": "5e-5", # 比 UNet 低
},
"采样设置": {
"enable_preview": True,
"prompt": "sks dog playing in park",
"num_samples": 4,
},
}

8.3 训练技巧#

def training_tips():
"""
SD LoRA 训练技巧。
"""
return {
"避免过拟合": {
"方法": "正则化数据 + 提前停止",
"正则化图像": "使用同主体 + 不同时期的图像",
"技巧": "当验证损失开始上升时停止训练",
},
"保持泛化": {
"方法": "多样化的训练数据",
"技巧": "不同角度、背景、光照的图像",
"警告": "只有正脸 → 无法生成侧脸",
},
"风格 vs 主体": {
"风格 LoRA": "推荐 rank=16-32,多样化数据",
"主体 LoRA": "推荐 rank=32-64,少量但高质量",
},
"Text Encoder 微调": {
"效果": "显著提升文本对齐能力",
"风险": "可能导致 CLIP 空间偏移",
"建议": "用更低的 learning rate",
},
"组合多个 LoRA": {
"方法": "多个 LoRA strength 加权叠加",
"技巧": "从低 strength 开始尝试",
"工具": "ComfyUI 的 LoRA Stack 节点",
},
}
def common_issues():
"""
常见问题与解决。
"""
return {
"过拟合 (生成与训练图完全相同)": {
"原因": "训练步数过多",
"解决": "减少步数,或增加正则化",
},
"风格错位 (生成的东西风格混乱)": {
"原因": "训练数据不够多样",
"解决": "增加更多角度/背景的数据",
},
"崩坏 / 灰度输出": {
"原因": "LoRA 强度过大",
"解决": "降低 strength (0.5-0.7)",
},
"只生成训练数据中的一张": {
"原因": "严重过拟合",
"解决": "增加数据多样性,减少步数",
},
"LoRA 不生效": {
"原因": "strength 设为 0,或加载失败",
"解决": "检查 strength 参数,确认文件正确",
},
}

9. LoRA 与其他技术的组合#

9.1 LoRA + ControlNet#

def lora_controlnet_combo():
"""
LoRA 和 ControlNet 的组合。
"""
return {
"效果": "LoRA 控制风格/主体 + ControlNet 控制结构",
"使用场景": "用特定角色 + 特定姿态生成图像",
"加载顺序": [
"1. 加载 SD Checkpoint",
"2. 加载 ControlNet (如 OpenPose)",
"3. 加载 LoRA (如特定角色)",
"4. 输入: prompt + 姿态图",
],
"示例": "LoRA (特定动漫角色) + ControlNet (特定姿态) → 同角色新姿态图",
}

9.2 LoRA + IP-Adapter#

def lora_ipadapter_combo():
"""
LoRA 和 IP-Adapter 的组合。
"""
return {
"IP-Adapter": "用图像作为额外条件控制生成",
"组合效果": "LoRA 学习风格 + IP-Adapter 参考图像",
"使用场景": "用同风格的参考图 + LoRA 生成新内容",
"技巧": "LoRA strength 高 + IP-Adapter strength 低",
}

9.3 多 LoRA 组合#

def multi_lora_combo():
"""
多个 LoRA 组合。
"""
return {
"方法": "在 WebUI/ComfyUI 中叠加多个 LoRA",
"公式": "W_final = W_base + α1·ΔW1 + α2·ΔW2 + α3·ΔW3",
"注意事项": [
"总参数量是各 LoRA 之和",
"strength 超过 1 可能导致崩坏",
"不同 LoRA 可能冲突",
],
"技巧": [
"同类型 LoRA 可以叠加",
"风格 LoRA + 角色 LoRA 可以",
"两个角色 LoRA 叠加会混合",
],
"推荐工具": "ComfyUI LoRA Stack 节点",
}

10. 总结#

10.1 核心要点#

维度关键要点
DreamBooth主体级微调,需要 3-5 张图,学习完整主体表示
Textual Inversion学习新词嵌入,代表简单概念,参数最少
LoRA低秩适配器,效率与效果平衡,社区最常用
LyCORISLoRA 变体集合(LoCon/LoHA 等),更精细的控制
SDXL LoRA需要同时微调双文本编码器,推荐更高 rank
社区生态Civitai 分享,ComfyUI 组合,Kohya_ss 训练

10.2 方法对比总览#

Textual Inversion DreamBooth LoRA LyCORIS
参数 ~1M ~1B ~1-10M ~2-15M
效果 ★★☆☆☆ ★★★★★ ★★★★☆ ★★★★☆
泛化 ★★★☆☆ ★★★★★ ★★★★☆ ★★★★☆
速度 快 慢 快 快
组合 简单 困难 简单 简单
适用 风格/图标 人物/宠物 通用 风格/细节

10.3 一句话总结#

SD LoRA 微调生态的本质是一个”知识注入”框架——DreamBooth 用少量图像教会模型”这是什么”,Textual Inversion 用一个向量表示”这个概念”,LoRA 则是在权重空间中找到一个高效方向来编码风格和概念,Civitai 上无数个 MB 级别的 .safetensors 文件背后,就是这些技术在开源社区中落地生根的成果——让每个人都能用消费级硬件训练出”二次元老婆""水墨风格""梵高画风”,并分享给全世界。

10.4 推荐资源#

论文:
- DreamBooth (Ruiz et al., 2023): "DreamBooth: Fine Tuning Text-to-Image Diffusion Models"
- Textual Inversion (Gal et al., 2022): "An Image is Worth One Word"
- LoRA (Hu et al., 2021): "LoRA: Low-Rank Adaptation of LLMs"
- LyCORIS: https://github.com/KohakuBlueleaf/LyCORIS
代码/工具:
- KohakuBlueleaf/LyCORIS: LyCORIS 实现
- kohya-ss/sd-scripts: Kohya_ss 训练脚本
- AUTOMATIC1111/stable-diffusion-webui: WebUI (支持 LoRA)
- comfyanonymous/ComfyUI: 节点式 LoRA 工作流
平台:
- Civitai: https://civitai.com (最大的 SD 模型分享平台)
- HuggingFace: Stable Diffusion 模型库
社区:
- Reddit r/StableDiffusion
- Civitai Discord

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Stable Diffusion LoRA 微调:从 Dreambooth 到社区生态的完整指南
https://aiattnstudio.link/posts/sd-lora-finetuning/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签