SDXL上的LoRA微调实战
1.准备阶段
1.1项目背景
近年来,以 Diffusion Models 为代表的生成模型,已经成为视觉生成领域的主流方法。以 Stable Diffusion 为代表的开源模型,使得普通开发者也可以在本地进行高质量图像生成与定制。
但一个核心问题始终存在:
通用模型很强,但不“认识你自己的数据”。
例如:
- 你有一个实验室设备的照片;
- 一个独特的工业零件;
- 或者像本项目中的 COIL-100 单个物体;
基础模型通常无法稳定复现这些“特定实例”。因此,就需要一种机制,让模型能够:
- 学会一个“具体对象”
- 并在不同场景中复现它
- 同时不破坏原模型能力
这正是本项目的核心动机。
1.2为什么选择LoRA微调?
在微调大模型时,传统方法存在明显问题:
- 全量微调成本极高(显存 + 时间)
- 容易过拟合
- 权重体积巨大,不便分享
为了解决这些问题,LoRA 被提出。LoRA 的核心思想是:
不直接修改原模型参数,而是学习一个低秩的“增量表示”。
其优势非常明显:
- 训练参数量极小
- 显存需求低(适合单卡)
- 可插拔(类似“插件”)
- 可以叠加多个 LoRA
这使它成为当前最主流的 Stable Diffusion 微调方案之一。
1.3为什么选择 SDXL
在 Stable Diffusion 系列中,Stable Diffusion XL 是一个关键里程碑版本。
相比早期模型:
- 分辨率提升到 1024×1024 原生支持
- 语义理解更强
- 细节表现更稳定
- 架构更适合个性化微调
此外,SDXL 采用“双阶段结构”:
- base:生成基础图像
- refiner:细节精修
本项目只在 base 上训练 LoRA,然后可选用 refiner 提升质量,这是目前实践中最稳定的组合。
1.4为什么选择COIL-100数据集?
本项目使用的数据来自 COIL-100。
该数据集有一个非常适合 LoRA 实验的特点:
- 每个物体 72 个视角
- 每 5° 一张
- 背景统一(黑色)
- 光照相对稳定
这带来两个关键研究价值:
优点
- 非常适合学习“物体本身”
- 多视角 → 有利于泛化能力
挑战
- 强背景偏置(黑背景)
- 容易过拟合“构图模板”
因此它是一个非常理想的教学级数据集:
既容易成功,又能暴露真实问题。
2.DreamBooth 原理分析
2.1 DreamBooth 的本质:把“稀有 token”和“具体实例”绑定
一个预训练扩散模型已经见过很多常见词,比如:
dogcupcarobject
这些词在文本编码器里都有稳定的语义表示,在图像生成过程中也有对应的视觉分布。
但你的特定物体并不在预训练语义空间里。
DreamBooth 的思路就是:
- 选一个原本几乎没有语义负担的词,作为触发词。例如:
zvqobj - 设计 prompt 模板,例如:a photo of zvqobj object
- 用这个 prompt 配合你的训练图反复训练
- 让模型逐渐学会:
当文本里出现
zvqobj时,应该把去噪过程引导到“这个具体物体”的图像分布附近
2.2 为什么必须有“类词”
DreamBooth 里通常不会只写:a photo of zvqobj
而是会写:a photo of zvqobj object
这里的 object 就是类词。
它的作用非常重要:
作用一:告诉模型“这个实例属于什么大类”
如果没有类词,模型只知道你在学习一个奇怪 token,但不知道它是杯子、玩具、机器零件,还是其他东西。有了 object 或更具体的 toy, bottle, cup 这类词,模型就能把:
- 新 token 的实例特征
- 已有类别的通用知识
结合起来。
作用二:减少训练难度
模型本来就知道 object 的大致视觉分布。所以训练时不需要从零学“它是什么类别”,只要学:
这个类别里有一个特别的具体实例,名字叫
zvqobj
2.3 DreamBooth 在训练时实际发生了什么
训练时,每一张图都配有类似的文本:a photo of zvqobj object
整个过程可以这样理解:
- 训练图先经过 VAE 编码到 latent space
- 给 latent 加噪声
- prompt 经过文本编码器变成文本 embedding
- UNet 接收 noisy latent 和文本条件,预测噪声
- 用预测噪声和真实噪声计算损失
- 反向传播,更新可训练参数
在这个过程中,模型逐步学到:
zvqobj不再是一个“空白 token”- 它应当对应这个训练物体的颜色、轮廓、结构和纹理
- 当它和
object一起出现时,应生成那个具体实例,而不是泛泛的普通物体
2.4 DreamBooth 的收益和风险
收益
- 少量图像就可以学一个具体实例
- 能让模型具备“个性化记忆”
- 对单物体、单人脸、单概念任务特别有效
风险
- 容易过拟合
- 容易把背景、光照、构图一起记住
- 容易把“实例特征”和“拍摄模板”混为一谈
这在 COIL-100 项目里尤其明显,因为数据有:
- 黑背景
- 居中物体
- 多视角但摄影条件单一
所以 DreamBooth 在这里不只是学习“这个物体”,也很可能学习:
- 黑背景
- 居中摆放
- 类似产品照的构图方式
这些缺陷在后续项目实战结果中有所体现。
3.LoRA原理分析
3.1 LoRA 的核心思想:低秩增量更新
先假设某一层原本有一个线性映射:
传统微调是直接把W改成新的参数。LoRA 不这样做。它保留原来的W,只额外学习一个增量:
关键在于,ΔW 不直接学成一个完整大矩阵,而是被拆成两个小矩阵:
其中:
A:把高维特征压到低维B:再把低维特征映射回原空间- 中间维度
r就是 rank
如果原矩阵是 d × k,直接学完整 ΔW 的代价很高;但如果只学低秩分解,就会小很多。
3.2 LoRA 在 SDXL 中的具体作用
在 SDXL 的生成过程中,核心的生成网络是 UNet。
LoRA 主要作用在 attention 相关线性层
在 Stable Diffusion / SDXL 这类模型里,LoRA 最常加在:
- self-attention 里的线性投影层
- cross-attention 里的线性投影层
- 有时也包括部分 feed-forward 中的线性层
最常见的一类位置是 attention 中的这些投影:
-
to_q -
to_k -
to_v -
to_out
也就是:
- Query 投影
- Key 投影
- Value 投影
- 输出投影
这些都是线性层,非常适合插入 LoRA。
3.3 为什么 attention 层特别重要
因为扩散模型的条件生成,本质上依赖“注意力如何分配”。
特别是 cross-attention:
它负责把“文本条件”注入到图像生成过程里。
可以粗略理解成:
- 文本 embedding 提供语义条件
- 图像 latent 提供当前视觉状态
- cross-attention 决定当前去噪步骤里,哪些文本信息该影响哪些视觉区域
所以当你训练一个 LoRA 去学习 zvqobj 时,本质上是在让模型学会:
当文本里出现
zvqobj,attention 该如何重排,才能生成那个具体物体
这就是为什么 LoRA 经常只加在 attention 相关层,就已经很有效。
3.4 LoRA 改的是什么能力
第一类:实例外观特征
- 物体整体轮廓
- 颜色分布
- 主要纹理
- 局部结构特征
第二类:视角一致性
你的训练集有 72 个角度,这会让模型学到:
- 这个物体从不同方向看应该怎样变化
- 哪些形状是稳定属性,哪些是视角变化导致的表观差异
第三类:文本触发关系
LoRA 还帮助建立:
- token
zvqobj - 视觉特征集合之间的映射关系
也就是说,它不是在“记一张图”,而是在学习:
“当读到这个 token 时,UNet 的注意力和特征变换应该往哪个方向偏移。”
4.训练细节设置
4.1 训练数据集预处理
每张图片都会经过
每张图会经历这些处理:
- EXIF 方向校正
- 转 RGB
- resize
- 随机裁剪到 1024 x 1024
- 归一化到 [-1, 1]
同时还记录:
- 原始尺寸 original_size
- 裁剪左上角坐标 crop_top_left
它们之所以要被记录,是因为 SDXL 不只看文本和图像像素,还会利用这类尺寸/裁剪元信息做 micro-conditioning,帮助模型理解图像的空间来源和构图上下文。这里的 micro-conditioning 可以理解成:除了文本 prompt 之外,再额外给模型一些“细小但重要的生成条件”。
Stable Diffusion的Unet除了latent,TimeSteps,prompt+embedding外还有一个输入为unet_added_conditions
unet_added_conditions = {
"time_ids": add_time_ids,
"text_embeds": ...
}
add_time_ids = torch.cat(
[
compute_time_ids(original_size=s, crops_coords_top_left=c)
for s, c in zip(batch["original_sizes"], batch["crop_top_lefts"])
]
)
def compute_time_ids(original_size, crops_coords_top_left):
# Adapted from pipeline.StableDiffusionXLPipeline._get_add_time_ids
target_size = (args.resolution, args.resolution)
add_time_ids = list(original_size + crops_coords_top_left + target_size)
add_time_ids = torch.tensor([add_time_ids])
add_time_ids = add_time_ids.to(accelerator.device, dtype=weight_dtype)
return add_time_ids
time_ids 负责告诉 UNet:
“这张训练图原始多大、裁剪从哪开始、当前目标尺寸是什么。”
compute_time_ids结果是将原始尺寸,随机裁剪左上角,训练分辨率合并成一个tensor。例如:
假设某张原图:
- 原始尺寸 original_size = (1200, 1200)
- 随机裁剪左上角 crop_top_left = (80, 120)
- 训练分辨率 resolution = 1024
那么:
compute_time_ids((1200, 1200), (80, 120))最终得到的是:
[[1200, 1200, 80, 120, 1024, 1024]]
text_embeds不是上面的 token 级 prompt_embedding,而是 pooled text embedding,也就是更全局的文本语义向量。
4.2 训练设置细节
prior preservation 模式:保持底模原本对这个类别的先验知识
在 DreamBooth 里,你通常会教模型一个新实例,比如:
zvqobj toy truck
这里:
- zvqobj 是实例标识
- toy truck 是类别词
如果你只拿 72 张目标物体图去训练,模型很可能会学到:
“只要看到 toy truck,就往你这台橙色小车靠”
这会带来一个问题:
模型可能不仅学到了你的实例,还会把底模原本的“toy truck”类别分布也拉偏。
这就叫类别漂移或者某种意义上的“过度绑定”。
prior preservation 的目的就是:
在学习 zvqobj toy truck 这个新实例的同时,不要破坏底模对一般 toy truck 的认知。
prior preservation 在训练里怎么做
方法很直接:训练时同时喂两类样本。
-
instance images
- 你的目标物体图
- prompt 类似:
a photo of zvqobj toy truck
-
class images
- 一般类别图,不是你的特定物体
- prompt 类似:
a photo of toy truck
然后训练时会同时算两部分 loss:
- instance loss
- prior loss
在 loss 代码中先将batch 一分为二:
model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0) target, target_prior = torch.chunk(target, 2, dim=0)
然后分别算:
- 实例 loss
- prior loss
最后加起来:
loss = loss + args.prior_loss_weight * prior_loss
本项目初次实验没有使用这种训练方式,因为这种训练方式需要额外准备class images。
SNR gamma加权
SNR 是:
Signal-to-Noise Ratio,信噪比
在扩散模型里,每个 timestep 的噪声强度不同:
- 早期 timestep:图像信号还比较强,噪声少
- 后期 timestep:几乎全是噪声,原始图像信息很弱
所以不同 timestep 上的训练样本难度不一样。
如果直接一视同仁地对所有 timestep 做普通 MSE,训练可能会被某些 timestep 主导,导致学习效率或效果不理想。
SNR gamma 加权想做什么
它的目标是:
重新平衡不同 timestep 的 loss 权重,让训练更稳定、更有效。
在官方脚本里,如果设置了 snr_gamma,loss 会走这一支:
核心逻辑是:
- 先算每个 timestep 的 snr
- 再根据 gamma 截断或缩放权重
- 用这个权重乘到每个样本的 MSE 上
代码里是:
snr = compute_snr(noise_scheduler, timesteps) base_weight = ( torch.stack([snr, args.snr_gamma * torch.ones_like(timesteps)], dim=1).min(dim=1)[0] / snr )
然后得到 mse_loss_weights,再加权到 loss 上。
gamma 是一个控制权重裁剪强度的超参数。常见经验值例如:5.0
gamma 越小,重加权越强烈;
gamma 越大,越接近普通训练。
SNR 的计算非常直接,核心公式就是:
SNR(t) = (alpha / sigma)^2
而这里的:
- alpha = sqrt(alphas_cumprod[t])
- sigma = sqrt(1 - alphas_cumprod[t])
在本次训练中我们方便起见也不使用这个选项。
其余训练操作及流程和正常扩散模型完全相同。
5. 训练结果
我们采用以下4个提示词分别用不同随机种子进行生成:
- a photo of zvqobj toy truck
- a studio product photo of zvqobj toy truck on white background
- a close-up photo of zvqobj toy truck on a wooden desk, soft daylight
- a dramatic product photo of zvqobj toy truck in a dark studio
默认生成参数:
- num_inference_steps = 30
- guidance_scale = 7.0
- LoRA scales = [0.6, 0.8, 1.0]
这里的LoRA scales作用是:在推理阶段缩放 LoRA 增量权重,控制微调概念对底模的影响强度。
作用原理:W' = W + scale * ΔW
在训练的过程中我们一般不称其为scale,我们使用lora_alpha来控制LoRA分支输出的缩放强度。
ΔW = (lora_alpha / r) * (B @ A),W' = W + ΔW
此时lora_alpha / r这个量实际充当了lora_scale的作用。r是两组低秩矩阵的rank。在本次实验中我们设置Lora_alpha=4,rank=4,即在训练时的lora_scale=1.0。

我们可以观察到经过微调后的模型可以正确体现很多数据集中的特征:包括“橙色”,“正方体车厢”,“特色车头”等。
我们依然发现了一些问题:
1.经过微调后生成的图像清晰度不足。
训练集原始分辨率只有 128×128 很可能是主要原因之一。SDXL 原生更适合在 1024×1024 尺度上生成和训练,而COIL-100 物体图像本身只包含很少的高频纹理、边缘和材质细节;训练时即使被 resize 到 1024,也只是把低分辨率信息放大,并不会凭空产生真实细节。因此 LoRA 学到的是一个低细节、低纹理密度的物体表示,微调后生成结果自然可能偏软、偏糊。
2.lora_scale调的过高会出现细节丢失的问题,特别是背景等细节元素,几乎全部丢失。
lora_scale本质是在放大 LoRA 学到的低秩增量。当 scale 较高时,UNet 中 attention projection 的改变量过强,模型会过度服从 “zvqobj 这个主体应该长什么样” 的实例约束,而削弱底模原本负责自然背景、复杂纹理和画面丰富度的生成能力。结果就是主体被强行突出,背景变得空洞、简化,甚至细节整体塌缩。
3.lora_scale调的过高倾向于直接拟合训练集中的某张图片。例如lora_scale=1.0条件下的第一和第三张图片可以明显发现训练集原始图像的痕迹。
这属于典型的小样本 DreamBooth-LoRA 过拟合。数据虽然有 72 张,但它们来自同一物体、同一拍摄环境、同一黑背景和固定构图,数据多样性实际很低。LoRA 在高 scale 下会强烈放大训练集中最稳定、最容易降低 loss 的视觉模式,因此生成结果容易落入某些训练图的“吸引子状态”,表现为角度、轮廓、背景或构图都明显像原始训练样本。
3.第四张图片出现了明显的“黑背景”。
这是 COIL-100 数据集偏置被一起学进去的结果。DreamBooth 并不会自动区分“主体”和“背景”,它只看到
zvqobj总是和黑背景共同出现,于是模型会把黑背景也当作该概念的一部分。lora_scale越高,这种条件绑定越强,所以黑背景更容易泄漏到生成结果中。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)