简介:这是一份面向深度学习开发者和研究人员的扩散模型微调实战项目,聚焦低秩适应(LoRA)技术,用于快速完成扩散模型的个性化训练,帮助中高阶学习者在有限算力下降低显存占用与训练成本,快速落地图像生成、风格转换等定制任务。压缩包共58个文件,约90.2MB,主体为17个脚本源码、6个交互式笔记,覆盖训练、推理、图生图、图像修复与低秩权重合并流程;另有10个示例模型权重、6个运行脚本及若干示例图片与说明文档,结构清楚,便于直接对照执行。项目按源码、权重、训练脚本和管理模块分目录组织,内含Dreambooth微调、文本编码器与扩散模型分阶段训练、权重合并与推理演示,并配有多种风格权重和图像修复案例,可帮助理解低秩适应如何降低更新参数规模,同时保持生成质量。目前已有441人学习下载,适合通过实际代码进一步掌握低秩适应原理,并将微调能力迁移到自己的图像生成项目中。
1. 微调Diffusion选LoRA,不是因为它新,而是因为它快
在扩散模型上微调一个风格,绝大多数团队遇到的第一个瓶颈不是显卡,而是试错次数。所谓大模型微调放到Diffusion语境里,就是对Stable Diffusion这类基础模型做局部参数更新;全量微调成本高、迭代慢,LoRA才是当前性价比最高的默认起点。用LoRA训练UNet,单张24GB显卡可以在半小时内完成一次风格实验,训练产物通常只有几十到两百MB,切换风格时底模不用动。这里的LoRA是低秩适配,和无线通信里的LoRa协议没有任何关系。下面从LoRA为什么低秩有效讲起,落到本地部署与训练的最小命令,最后以一组排查路径收尾。不管你做stable diffusion本地部署、产品化风格迁移,还是研究多模态微调里更小的微调单位,这套方法都能直接上手。
2. LoRA低秩适应原理:不改基础模型也能改画风,关键是改谁、怎么改
2.1 Stable Diffusion生成链路里,LoRA真正改动的是UNet注意力投影
Stable Diffusion pipeline里有三个权重主体:CLIP文本编码器、UNet和VAE。生成时,VAE把图像编码到latent空间,UNet在这个空间里做多步去噪,CLIP负责提供文本语义。你想要的画风、构图、光照控制,绝大多数最终都表达在UNet的cross-attention层:文本作为key和value,图像特征作为query,两者在做跨模态匹配。LoRA训练最常见的做法,是把低秩矩阵挂在UNet的四个投影上:
- to_q:图像侧query投影,决定“当前图像块关注文本里的哪个词”;
- to_k / to_v:文本侧key/value投影,决定“文本信息如何注入图像”;
- to_out.0:注意力输出投影,影响融合后的信息如何写回图像特征。
这四个位置在不同层里重复出现,浅层管空间结构,深层管语义细节。训练时冻结原权重,只更新挂在旁边的低秩矩阵;推理时LoRA权重可以随时拔掉,模型立刻回到原来的画风。这就是“不改基础模型也能改画风”的机制来源:不是换底模,而是在注意力通路上加一条可控的旁路。
2.2 低秩分解如何用两个小矩阵装下微调增量
全量微调需要更新一个大矩阵W,形状是(in_features, out_features):假设是768×768,一份微调增量ΔW就有58万个参数。LoRA把ΔW拆成两个小矩阵的乘积:ΔW = (α / r) · B · A,A的形状是(r, out_features),B是(in_features, r)。两矩阵相乘后的形状与原权重一致,但参数量从in×out降为r×(in+out)。当r=16时,768×768矩阵的参数量只有原来的约1/24。
B初始化为零,A用高斯分布初始化,所以训练第一步模型输出和底模完全一致,不会因为插入了旁路而突然劣化。这里的α是缩放系数,不是rank本身;实际生效的scale等于α/r。很多教程把alpha直接设成和rank相等,只是为了不改默认scale,并不意味着这个值一定最优。
低秩有效的直觉解释是:扩散模型微调时,真正需要的权重变化集中在少数几个方向上,尤其是注意力投影的高频方向。用低秩旁路描述这些主方向,已经能覆盖绝大部分效果;强行提高rank,更多是记住训练集里的噪声,而不是学到可迁移的画风。这也是LoRA在Stable Diffusion上比全量微调更不容易过拟合的原因之一。
2.3 LoRA微调的选型:diffusers官方脚本、kohya还是ComfyUI
常见的落地路径有三条。diffusers官方脚本是纯命令行,适合批量实验和接入CI,输出pytorch_lora_weights.safetensors,WebUI和ComfyUI都可以直接加载;缺点是脚本更新快,参数名随版本变动。kohya_ss是社区最成熟的训练集,GUI和CLI都有,对正则化集、SDXL、多概念的支持最完整,适合数据量大、要到具体业务精度的情况。ComfyUI的训练节点适合已经在ComfyUI里搭好出图工作流的人,训练完立刻切到采样工作流出图,缺点是调参脚本化能力弱,不适合大规模并行跑实验。
我的做法是:实验初期用diffusers官方脚本,因为命令清晰、报错容易搜;进入量产阶段再挪到kohya的配置里补正则化。如果你的目标只是给产品出一个风格LoRA,diffusers已经够用,不需要为GUI额外学一套交互。选型时还要考虑下游加载环境:服务端用diffusers加载,WebUI/ComfyUI用户多,最好训练完都用不同端各加载一次验证,不要默认兼容。
2.4 SDXL与SD1.5在LoRA训练上的区别
SDXL的UNet比SD1.5大得多,而且提示理解依赖两个文本编码器。SD1.5 LoRA通常只训练UNet;SDXL做LoRA时,如果完全不碰文本编码器,风格可控性会明显弱一截,很多团队会把OpenCLIP和CLIP ViT-L都加上LoRA,显存开销也随之上去。
| 对比项 | SD1.5 | SDXL |
|---|---|---|
| UNet参数量 | 约860M | 约2.6B |
| 常用分辨率 | 512×512 | 1024×1024 |
| 单卡LoRA显存参考 | 12GB可跑 | 24GB建议起步 |
| 文本编码器 | 一般不训练 | 常配OpenCLIP一起训练 |
| 输出LoRA通用性 | WebUI/ComfyUI/diffusers都兼容 | 同样兼容,注意基座版本一致 |
这里最关键的一条是:LoRA不能跨底模。把SD1.5训练出来的权重挂到SDXL上,加载时会报权重尺寸不匹配或不生效;即使不报错,也基本无效。项目源码里如果同时出现两个版本的训练脚本,先确认你要复用的是哪个底模再开跑。
3. 用diffusers在本地把LoRA微调跑起来:最小环境、训练命令与出图验证
3.1 环境安装与底模准备
第一个坑是PyTorch和CUDA版本不匹配。先看nvidia-smi里的CUDA版本,再装对应轮子;下面以CUDA 12.1为例:
# 创建独立虚拟环境,避免污染系统Python python -m venv .venv source .venv/bin/activate # 安装与CUDA 12.1匹配的PyTorch;换卡时把cu121改成对应版本 pip install torch --index-url https://download.pytorch.org/whl/cu121 # 剩下的库直接装最新发布版 pip install diffusers transformers accelerate peft safetensors说明:accelerate负责多卡和混合精度,peft是LoRA底层实现,diffusers提供训练脚本和推理pipeline。版本策略上不要追每日版,发布版最稳;报“算子不存在”的错,优先查PyTorch和CUDA,而不是diffusers。
下载并准备底模:
# 把SD1.5权重下载到本地,后续训练和推理都指向这个目录 huggingface-cli download stable-diffusion-v1-5/stable-diffusion-v1-5 --local-dir ./models/sd15底模下载一次即可复用。每次训练不要改这个目录里的任何文件,LoRA训练过程中只读取,不会写入。
3.2 用官方训练脚本跑LoRA的最小命令
下载diffusers仓库,安装依赖后直接用官方文本转图像LoRA脚本:
git clone https://github.com/huggingface/diffusers cd diffusers pip install . # 配置accelerate;单卡直接选no、1、no、fp16即可 accelerate config # 最小训练命令:dataset目录结构见4.1节 accelerate launch examples/text_to_image/train_text_to_image_lora.py \ --pretrained_model_name_or_path=./models/sd15 \ --train_data_dir=./dataset \ --resolution=512 \ --train_batch_size=2 \ --gradient_accumulation_steps=4 \ --max_train_steps=2000 \ --learning_rate=1e-4 \ --lr_scheduler=cosine \ --rank=32 \ --checkpointing_steps=500 \ --output_dir=lora-output \ --mixed_precision=fp16命令拆开看:train_batch_size=2是单步进GPU的图数,gradient_accumulation_steps=4让优化器每4步攒起8张图的梯度再更新一次,显存不够时优先加这个而不是减batch;rank=32是LoRA的秩,质量与显存的折中值;max_train_steps=2000对10到50张图足够;mixed_precision=fp16把激活值切到半精度,LoRA可训练参数仍然是fp32更新,兼顾显存和稳定。checkpointing_steps=500每500步存一个可恢复的训练快照,不是最终LoRA文件。
如果显存低于12GB,把train_batch_size改成1,gradient_accumulation_steps改成8,同时加--gradient_checkpointing,训练步数不变。
3.3 训练过程中的显存观测与模型产物
训练时另开一个终端看显存:
watch -n 2 nvidia-smi正常训练时显存占用会稳定在一个值附近,突然暴涨说明某个时间步的激活值异常,多半是分辨率与底模不一致或文本描述里有异常字符。训练结束后,输出目录里会多出:
lora-output/ ├── checkpoint-500/ ├── checkpoint-1000/ ├── checkpoint-1500/ └── pytorch_lora_weights.safetensorspytorch_lora_weights.safetensors就是最终LoRA权重,几十到两百MB,和底模完全分离。中间checkpoint用--resume_from_checkpoint可恢复训练;如果只是想出图,不要加载checkpoint目录,直接加载最后的LoRA文件。
3.4 训练完立刻用LoRA权重出图
训练到一半想看效果,另写一个推理脚本:
from diffusers import StableDiffusionPipeline import torch pipe = StableDiffusionPipeline.from_pretrained("./models/sd15", torch_dtype=torch.float16).to("cuda") pipe.load_lora_weights("lora-output", weight_name="pytorch_lora_weights.safetensors") # 固定随机种子,多次训练之间才能横向对比 g = torch.Generator(device="cuda").manual_seed(42) image = pipe( "a cyberpunk cafe storefront at night, <cybercafe>", num_inference_steps=28, guidance_scale=7.0, generator=g, cross_attention_kwargs={"scale": 0.8}, ).images[0] image.save("sample_from_lora.png")cross_attention_kwargs里的scale是LoRA权重生效比例,默认1.0;调试时从0.6开始往上加,能找到“风格够明显但还保留底模自由度”的点。出图后发现风格没变化,第一件事不是加训练步数,而是检查load_lora_weights是否真的执行成功;权重加载成功但效果弱,才去调scale和训练参数。
4. 数据集与超参数配置:LoRA训练“看起来在loss下降、实则学歪了”的高发区
4.1 数据集组织:同名txt与imagefolder格式
官方脚本读--train_data_dir时,期望目录下每张图片配一个同名txt:
dataset/ ├── 001.png ├── 001.txt ├── 002.png ├── 002.txt └── 003.png ...txt里写这一张图的完整描述。一个批量生成描述文件的做法:
for f in dataset/*.png; do base="${f%.png}" # 固定风格词放在句尾,主体词按图单独改 echo "a storefront at night, neon sign, <cybercafe>" > "${base}.txt" done注意脚本会把prompt裁剪到77个token,超过的部分直接截断。如果一句话写到80个token以上,后半段文本在训练时根本没参与;LoRA只会记住前半段信息。所以描述文本要短,黄金长度在20到50个token之间。
4.2 描述文本该写到什么程度:固定风格词、变化主体词
LoRA训练里最常见的翻车是“描述写得太满”。每张图都堆“best quality, masterpiece, 8k, ultra detailed”这类质量词,LoRA会把这些词和画风绑在一起;推理时你想让模型画一个训练集里没有的主体,质量词会抢走风格词的注意力。另一个极端是整组图共用一句话,风格特征和主体特征严重耦合,生成新构图时画风控制力很弱。
正确的做法是把描述拆成两部分。前面是随图片变化的主体描述,比如“a street corner, a railway bridge, a bicycle parked by the wall”;句尾固定放同一个风格触发词,比如<cybercafe>。触发词用带尖括号的组合词可以减少和现有词汇冲突。同一批数据集里触发词必须完全一致,大小写和空格都不能变。无论你看到的中文LoRA教程用的触发词是英文还是中文,关键都是触发词在训练和推理时保持逐字符一致。
提示:判断过拟合的时候,不要只看loss,要把生成图和训练图放在一起对照。loss持续走低但生成图失去多样性,往往是LoRA在背数据集而不是学风格。
4.3 不同数据规模下的三组参考参数
没有一套参数通吃所有风格。基于常见LoRA训练实践,给三组起步值:
| 数据规模 | rank参考 | 学习率参考 | 建议步数 | 备注 |
|---|---|---|---|---|
| 10-20张 | 16 | 2e-4 | 300-800 | 必须早停,很容易过拟合 |
| 30-80张 | 32 | 1e-4 | 1000-2000 | 大多数风格类项目的默认区间 |
| 100张以上 | 64 | 5e-5到1e-4 | 2000-5000 | 建议加正则化数据集 |
学习率不是越小越好,要配合步数看。2e-4配500步,和1e-4配1000步,在数学上不完全等价,因为调度器曲线不同;最稳的做法是固定一组参数,只调rank或只调学习率,不要三个变量同时改。判断过拟合的标准不是loss升,而是训练集图能复现、描述文本换成新概念后风格还在不在:风格没了但主体很清晰,说明低秩旁路把物体背了下来。
4.4 正则化数据集:让LoRA学风格而不是背物体
只有目标风格图时,LoRA很容易把“风格+特定主体”当做一个整体学会。比如你用100张猫的插画训练,推理时写“a dog in this style”可能还是出猫。解决办法是加入正则化数据集:用底模自己生成一批与目标图相同描述、但内容不同的图。目标图描述是“a cat,