Stable Diffusion微调实战:把AI画风调成你的专属滤镜,顺带踩坑埋

Stable Diffusion微调实战:把AI画风调成你的专属滤镜,顺带踩坑埋雷全记录

“我不想再抽到别人的老婆了,我要AI只画我的猫。”
——某位凌晨三点还在调学习率的前端同胞

如果你也曾对着Stable Diffusion生成的“千人一面”直翻白眼,恭喜你,已经摸到微调这扇大门的把手。本文不是论文,不是广告,更不是“三分钟包会”的短视频脚本,而是一篇来自加班狗的血泪笔记:从“为啥要微调”到“怎么把微调模型塞进React按钮”,顺带把路上踩过的坑挨个埋好,方便后来人直接平地起飞。全文一万字起步,代码管饱,注释管够,显卡风扇管转——请自备咖啡,我们慢慢聊。


为什么非得折腾微调?因为“默认脸”实在看吐了

Stable Diffusion开源社区里流传着一句黑话:
“不微调,你永远在抽卡;微调完,卡池里只剩你老婆。”

话糙理不糙。原生模型再强,也是用LAION-5B这种“大锅烩”练出来的,画风杂糅到像食堂大杂烩——偶尔好吃,经常翻车。
想要“只画我家橘猫”“只画我们品牌IP的二次元娘”“只画老板头像的Q版”,除了微调,没有第二条路。

更现实一点:

  1. 甲方爸爸要求“画风统一”,不能今天宫崎骏明天哥斯拉。
  2. 前端项目里要“实时出图”,不能让用户抽到奇行种。
  3. 显存只有12 G,全量微调直接OOM,得想办法“少吃多跑”。

于是,人人都在喊“我要微调”,可真正跑通pipeline还能把模型塞进Web页的,十不存一。下文就把“从0到线上可点按钮”拆成七步,每一步都给出可直接复制的脚本,以及“如果报错xxxx怎么办”的急救小纸条。


主流微调方法全家桶:Dreambooth、LoRA、Textual Inversion、全量微调,谁才是你的菜?

先放结论,再讲故事:

方案 显存占用 训练时长 画风记忆 提示词依赖 推荐指数(个人向)
Dreambooth 16 G+ 30 min~2 h ★★★★★ ⭐⭐⭐⭐☆
LoRA 8 G 10~30 min ★★★★☆ ⭐⭐⭐⭐⭐
Textual Inversion 6 G 5~15 min ★★☆☆☆ 极高 ⭐⭐☆☆☆
Full Fine-tune 24 G+ 半天~一天 ★★★★★ ⭐⭐☆☆☆(穷鬼劝退)

Dreambooth:把“猫娘”概念硬塞进模型权重,效果最稳,但吃显存吃到哭。
LoRA:在注意力旁路插“小辫子”,冻结主干,只训旁路,省显存、省时间,还能多LoRA叠加,前端最爱。
Textual Inversion:只学一个新token Embedding,模型权重纹丝不动,训练最快,但也最容易“记不住细节”。
Full Fine-tune:土豪玩法,除了展示公司财力,一般不建议。

下文实战以“LoRA”为主 Dreambooth为辅,原因无他:

  • 单人单卡2080Ti也能跑;
  • 导出.safetensors才几十兆,前端包体积友好;
  • 支持WebUI、ComfyUI、diffusers全生态,一把梭。

第一步:数据准备——别急着跑代码,先给AI喂“干净饭”

1.1 拍图 or 搜图?数量与质量黄金区间

经验值:

  • 目标主体“橘猫”→30~50张即可,太多反而过拟合;
  • 目标主体“二次元原创角色”→最好80+,角色三视图、表情包、服饰细节全覆盖;
  • 分辨率统一512×512以上,PNG/JPG都行,别整webp,部分脚本会跪。

1.2 自动去重+裁剪一条龙脚本

# dedup_resize.py
from pathlib import Path
from PIL import Image, ImageFile
import imagehash, shutil

ImageFile.LOAD_TRUNCATED_IMAGES = True

def dedup_and_resize(src_folder: Path, dst_folder: Path, size=512):
    dst_folder.mkdir(exist_ok=True)
    hashes = set()
    for img_path in src_folder.rglob("*"):
        try:
            img = Image.open(img_path).convert("RGB")
            h = imagehash.average_hash(img, 8)
            if h in hashes:
                continue                     # 重复图直接扔
            hashes.add(h)
            img = img.resize((size, size), Image.LANCZOS)
            img.save(dst_folder / img_path.name, quality=95)
        except Exception as e:
            print("❌ 无法处理", img_path, e)

if __name__ == "__main__":
    dedup_and_resize(Path("raw_imgs"), Path("clean_imgs"))

跑完你会得到一份clean_imgs/文件夹,体积清爽,哈希唯一。

1.3 手动打标签——别嫌麻烦,这是控制提示词的灵魂

LoRA训练需要“图文对”。懒人做法:让BLIP自动标注,再人工过一遍。

# 安装koboldcpp-blip Caption工具
pip install koboldcpp-blip
python -m koboldcpp_blip --input clean_imgs --output captions

生成得到同名.txt文件,内容类似a orange cat sitting on grass
接着人工批量替换:

  • 把“orange cat”统一成触发词ohmycat(触发词越独特越好,避免跟原模型token冲突);
  • 如果想让模型记住猫穿“小披风”,就在对应图里手动加ohmycat wearing red cape

小技巧:触发词+描述词长度≤75 token(SD1.x限制),留点余量给后面推理加装饰词。


第二步:环境搭好——一次装对,省得后面抓鬼

# 新建conda环境,Python3.10最稳
conda create -n lora python=3.10 -y
conda activate lora

# 核心三件套
pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 -f https://download.pytorch.org/whl/torch_stable.html
pip install transformers accelerate diffusers==0.18.2
pip install -U xformers  # 显存省30%

# 训练仓库用kohya_ss,社区最活跃
git clone https://github.com/kohya-ss/sd-scripts.git
cd sd-scripts
pip install -r requirements.txt

装完跑一把python -m pytest tests/——全绿就继续,有红就对着GitHub issue搜,99%是CUDA版本没对齐。


第三步:LoRA训练超细颗粒度拆解——参数怎么填、学习率踩雷实录

3.1 目录结构(强迫症福音)

project/
├─ clean_imgs/               # 图片
├─ captions/                 # 同名txt
├─ output/                   # 训练产出
├─ reg/                      # 可选正则化图(防止过拟合)
└─ train.sh                  # 一键启动

3.2 训练脚本(可直接抄,含中文注释)

# train.sh
export MODEL_NAME="runwayml/stable-diffusion-v1-5" # 基模
export IMG_ROOT="clean_imgs"
export CAPTION_ROOT="captions"
export OUTPUT_DIR="output/ohmycat_lora"
export RESOLUTION=512
export TRAIN_BATCH_SIZE=1
export GRADIENT_ACCUMULATION_STEPS=4   # 相当于batch=4
export MAX_TRAIN_STEPS=800
export LEARNING_RATE="1e-4"
export LR_SCHEDULER="cosine_with_restarts"
export LR_WARMUP_STEPS=100
export NETWORK_DIM=64                  # LoRA rank,越高越能吃细节
export NETWORK_ALPHA=32                # 平滑系数,一半rank即可
export SAVE_STEPS=200                  # 每200步存一次,方便回滚
export CAPTION_DROPOUT=0.05            # 随机丢弃caption,防过拟合

accelerate launch --num_cpu_threads_per_process 8 train_network.py \
  --pretrained_model_name_or_path=$MODEL_NAME \
  --train_data_dir=$IMG_ROOT \
  --output_dir=$OUTPUT_DIR \
  --resolution=$RESOLUTION \
  --batch_size=$TRAIN_BATCH_SIZE \
  --gradient_accumulation_steps=$GRADIENT_ACCUMULATION_STEPS \
  --max_train_steps=$MAX_TRAIN_STEPS \
  --learning_rate=$LEARNING_RATE \
  --lr_scheduler=$LR_SCHEDULER \
  --lr_warmup_steps=$LR_WARMUP_STEPS \
  --use_8bit_adam \
  --network_module=networks.lora \
  --network_dim=$NETWORK_DIM \
  --network_alpha=$NETWORK_ALPHA \
  --save_steps=$SAVE_STEPS \
  --caption_dropout_rate=$CAPTION_DROPOUT \
  --mixed_precision="fp16" \
  --xformers \
  --cache_latents \
  --gradient_checkpointing

3.3 常见翻车现场

症状 大概率原因 速效救心丸
loss 降到0.03 突然NaN 学习率太高 降到5e-5
显存依旧爆炸 batch_size=1也炸?把resolution降到384先跑通,再开梯度检查点
生成图全灰 正则化图缺失+步数太多 → 模型崩。加–num_reg_images=200,或干脆降步数到500

第四步:模型出炉,怎么知道“训好了”?

4.1 实时验证脚本(训练边跑边抽卡)

# validate.py
import torch
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler

pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16
)
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)

# 加载最新LoRA
lora_path = "output/ohmycat_lora/ohmycat_lora-800.safetensors"
pipe.unet.load_attn_procs(lora_path)

prompt = "ohmycat wearing sunglasses, sitting on beach, sunset"
image = pipe(prompt, num_inference_steps=20, guidance_scale=7.5).images[0]
image.save("validate_800.jpg")

肉眼观察:

  • 猫的品种、毛色跟你训练图是否一致?
  • 触发词ohmycat下,猫以外的元素(背景、道具)是否自由变化?

如果“猫固定、背景多变”,恭喜,模型已收敛;
如果“猫变成狗”→触发词冲突,换词重练;
如果“猫永远同一姿势”→过拟合,加正则化图、降步数、加caption_dropout。


第五步:Dreambooth备选方案——显存够就上,效果确实更顶

LoRA 800步能出80分效果,Dreambooth 1200步能冲95分,但16 G显存是门票。
前端同学如果公司报销A100,可直接冲。

关键差异(其余参数类似):

  • --train_text_encoder打开,让文本编码器也一起微调;
  • 学习率要更低(5e-6),不然文本编码器分分钟NaN;
  • 产出不是一个.safetensors,而是全新model_index.json整包,体积2~4 G。

前端集成时,Dreambooth模型走“整包替换”,LoRA可走“即插即拔”,后面会细讲。


第六步:前端如何把“微调成果”塞进网页按钮?

6.1 技术选型:全栈JavaScript也能玩推理

方案A:纯前端ONNX

  • 把LoRA权重合并到UNET,转ONNX,再转TensorFlow.js/WebGPU;
  • 优势:无服务器成本,离线跑;
  • 劣势:第一次加载模型200 M,流量费感人;且WebGPU支持率≈Chrome亲儿子。

方案B:小水管后端+React按钮(推荐)

  • 后端FastAPI加载diffusers,走GPU推理,前端纯调API;
  • 优势:前端代码无CUDA依赖,部署简单;
  • 劣势:需要一张GPU云服务器。

下文以方案B为例,因为——
“让用户的浏览器去跑Stable Diffusion,风扇声会吓退99%甲方。”

6.2 后端FastAPI(单文件,含缓存、队列、错误重试)

# main.py
import io, uuid, time
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import torch
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler

app = FastAPI()
device = "cuda"

# 全局加载一次模型,省显存
pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
    safety_checker=None  # 省显存,自己加过滤
)
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)

# 挂载LoRA,可热插拔
LORA_PATH = "output/ohmycat_lora/ohmycat_lora-800.safetensors"
pipe.unet.load_attn_procs(LORA_PATH)
pipe = pipe.to(device)

class GenReq(BaseModel):
    prompt: str
    width: int = 512
    height: int = 512
    steps: int = 20
    guidance: float = 7.5
    seed: int = -1

@app.post("/txt2img")
def txt2img(req: GenReq):
    if req.seed == -1:
        req.seed = int(time.time())
    generator = torch.Generator(device=device).manual_seed(req.seed)
    try:
        image = pipe(
            req.prompt,
            width=req.width,
            height=req.height,
            num_inference_steps=req.steps,
            guidance_scale=req.guidance,
            generator=generator
        ).images[0]
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
    buf = io.BytesIO()
    image.save(buf, format="PNG")
    buf.seek(0)
    return StreamingResponse(buf, media_type="image/png")

# 健康检查
@app.get("/hc")
def hc():
    return {"status": "ok"}

启动:

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1

6.3 React组件(Next.js示范,自带loading、错误提示)

// components/OhmycatGenerator.tsx
import { useState } from 'react'

export default function OhmycatGenerator() {
  const [prompt, setPrompt] = useState('ohmycat wearing christmas hat')
  const [loading, setLoading] = useState(false)
  const [img, setImg] = useState('')
  const [error, setError] = useState('')

  const generate = async () => {
    setLoading(true)
    setError('')
    try {
      const res = await fetch('/api/txt2img', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt, width: 512, height: 512 })
      })
      if (!res.ok) throw new Error(await res.text())
      const blob = await res.blob()
      setImg(URL.createObjectURL(blob))
    } catch (e: any) {
      setError(e.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="p-4">
      <textarea
        className="w-full textarea textarea-bordered"
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
      />
      <button
        className="btn btn-primary mt-2"
        onClick={generate}
        disabled={loading}
      >
        {loading ? '生成中…' : '抽一只猫'}
      </button>
      {error && <div className="text-red-500 mt-2">{error}</div>}
      {img && (
        <div className="mt-4">
          <img src={img} alt="result" className="rounded shadow" />
        </div>
      )}
    </div>
  )
}

前端页面秒级出图,后端单次512×512/20步≈2.5 s(RTX 3080)。
如果想再提速,把--num_inference_steps降到15,或者在后端加cache——相同prompt直接读缓存。


第七步:踩坑大全——从“ CUDA out of memory”到“猫怎么又变成狗”

坑位 症状 根因 急救
OOM 训练/推理直接炸 batch_size、resolution、attention同时飙高 先降resolution→384,再开gradient_checkpointing,再开xformers,再不行换LoRA
NaN Loss loss曲线突然升天 学习率太高/文本编码器炸 降LR到5e-5,关闭text_encoder训练,或加clip_grad_norm=1.0
过拟合 同一背景、同一姿势 训练图太少/步数太多/caption_dropout太少 加正则化图200张,步数砍半,caption_dropout提到0.1
概念漂移 狗混入猫包 触发词太普通,跟原模型token冲突 触发词加特殊前缀xyz_ohmycat,重新打标
灰图生成 全灰/全黑 训练崩/scheduler不兼容 换回DDIM试,确认vae是否损坏
推理慢 用户等到睡着 没开xformers,没缓存latent 后端加cache,开xformers,换DPMSolver

第八步:效率翻倍的黑科技——数据清洗到云端部署一条龙

8.1 数据清洗自动化(加Webhook,推送到Hugging Face Dataset)

# auto_upload.py
from huggingface_hub import HfApi
import shutil, datetime

api = HfApi()
zip_name = f"ohmycat_{datetime.date.today()}.zip"
shutil.make_archive(zip_name[:-4], 'zip', "clean_imgs")
api.upload_file(
    path_or_fileobj=zip_name,
    path_in_repo=f"data/{zip_name}",
    repo_id="你的用户名/ohmycat-dataset",
    repo_type="dataset",
    token="你的HF Token"
)

每晚定时跑,数据版本可回溯,队友再也不用“谁动了我的图”吵架。

8.2 Hugging Face Spaces秒级部署体验版

把上面FastAPI代码塞到SpacesDocker SDK模板,Dockerfile写两行:

FROM pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]

push上去,HF送你免费GPU 16 G,30分钟就能让甲方在线体验。
(记得把模型文件放Git-LFS,不然仓库爆炸。)

8.3 ControlNet加持——让猫精准坐在画面左下角

LoRA管“画风”,ControlNet管“构图”。
把Canny边缘图喂给ControlNet,prompt再写ohmycat,就能让猫按你给的轮廓摆pose。
前端调用只需多传一张binary图片,后端把ControlNet条件并联进pipeline即可,推理时间+30%,精准度+200%。


第九步:可复现、可协作、可上线的MLOps小作坊

9.1 版本管理:不止代码,模型、数据、参数全要管

  • 代码:git
  • 模型:git-lfs or DVC
  • 数据:HF Dataset + tag
  • 参数:把train.sh里所有变量写进config.yaml,训练时hydra自动读,保证每次commit都能复现。

9.2 实验记录:Notion+MLflow双保险

  • MLflow跟踪loss、学习率、最终FID;
  • Notion写“人话”结论:800步猫毛最蓬松,1200步猫变胖,甲方喜欢胖猫→上1200。

9.3 云端训练:从Colab到AWS SageMaker

  • Colab Pro+:RTX V100 16 G,一晚5美元,适合demo;
  • SageMaker:G5.xlarge 24 G,按需计费,CI/CD一键部署,适合生产。

train.sh包成sagemaker-training-job,数据集从HF拉,训练完自动推HF Hub,全流程无人值守。


写在最后:别只调模型,也调调自己的工作流

微调Stable Diffusion不是玄学,胜似玄学:
同样的30张猫图,有人出图是“ins滤镜”,有人出图是“克苏鲁”。
差别不在人品,而在“数据→参数→验证→迭代”的闭环有没有跑通。

把本文脚本全盘抄走,你至少能领先80%野生玩家;
再把MLOps流程跑顺,你就能让老板相信:
“AI画风定制”不是魔术,而是可排期、可上线、可回滚的普通需求。

下一回,当你凌晨三点看到猫咪高清大图从网页里蹦出来,
别忘了给显卡风扇一个拥抱——
它才是这段深夜加班故事里,最拼命的打工猫。

(全文完,代码已开源,愿世间再无“千人一面”。)

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐