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 loss2.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 参数) - 可以组合多个概念 embedding3.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 train3.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 torchimport torch.nn as nnimport torch.nn.functional as Ffrom 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 model4.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 out5.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_out5.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 | 低秩适配器,效率与效果平衡,社区最常用 |
| LyCORIS | LoRA 变体集合(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文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

