AI漫剧制 | 第 13 天:LoRA 训练——打造专属漫画角色

第 13/60 天

引言

在 AI 漫画创作中,最核心的难题之一就是角色一致性——如何让同一个角色在不同分镜、不同场景、不同角度下保持统一的面貌?第 5 天我们探讨了角色一致性的概念,而今天要介绍的 LoRA(Low-Rank Adaptation) 正是解决这一问题的终极武器。

LoRA 最初是作为大语言模型微调技术出现的,但很快被 Stable Diffusion 社区采纳,成为定制化图像生成的事实标准。通过训练一个轻量级的 LoRA 模型(通常 10-200MB),你可以让 AI 学会特定角色的面部特征、服装风格、画风笔触,甚至某种构图偏好。相比 DreamBooth 的全模型微调(2-7GB),LoRA 训练速度快 10 倍、文件体积小 100 倍,且可以叠加多个 LoRA 实现多角色控制。

本文将带你从零开始,完成一个完整的漫画角色 LoRA 训练流程,包括数据集准备、参数配置、训练执行和推理应用。

核心概念

LoRA 的工作原理

LoRA 在 Stable Diffusion 的交叉注意力层(Cross-Attention Layers)中注入低秩矩阵,用极小的参数量来”偏移”原模型的权重分布。数学上,原本的权重更新 ΔW ∈ ℝ^(d×k) 被分解为两个低秩矩阵 A ∈ ℝ^(d×r) 和 B ∈ ℝ^(r×k),其中 r 远小于 min(d,k):

W' = W + BA

在推理时,这个轻量级权重矩阵与基础模型合并,实现角色特征的注入,而几乎不增加推理时间。

关键训练参数

参数 推荐值 说明
rank (r) 16-64 秩越高,模型能记住的细节越多,但过拟合风险也越大
learning_rate 1e-4 AdamW 优化器的典型学习率
train_batch_size 1-4 取决于 GPU 显存(8GB 显存建议 batch=1)
max_train_steps 1000-2000 50-100 张图片通常 1500 步足够
resolution 512/768 与基础模型训练分辨率一致
network_dim 128 SDXL 推荐 128,SD 1.5 推荐 64

LoRA vs 其他方案对比

方案 文件大小 训练时间 角色一致性 可叠加性
LoRA 10-200MB 15-30min ★★★★☆ ✅ 可叠加多个
DreamBooth 2-7GB 60-120min ★★★★★ ❌ 独占
Textual Inversion 10-100KB 20-40min ★★☆☆☆ ✅ 可叠加
IP-Adapter 100-500MB 无需训练 ★★★☆☆ ✅ 可叠加

实战步骤

步骤 1:环境准备

首先,安装训练所需的依赖。推荐使用 Kohya_ss GUI 或直接使用 Diffusers 库进行训练。

# 创建训练环境
python3 -m venv lora_train_env
source lora_train_env/bin/activate

# 安装核心依赖
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
pip install diffusers accelerate transformers xformers
pip install datasets pillow wandb
pip install bitsandbytes

# 验证安装
python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}, GPU: {torch.cuda.get_device_name(0)}')"

步骤 2:数据集准备

数据集的质量直接影响 LoRA 的训练效果。以下是数据集准备的最佳实践:

# dataset_prepare.py — 数据集预处理脚本
import os
import json
from PIL import Image
from pathlib import Path

# 数据集目录结构
# dataset/
# ├── character_name/
# │   ├── 001.png
# │   ├── 001.txt      # 对应的提示词
# │   ├── 002.png
# │   └── 002.txt
# └── metadata.jsonl

def prepare_dataset(character_name, image_dir, output_dir):
    """
    漫画角色数据集预处理
    - 统一分辨率
    - 生成标注文件
    - 验证图片完整性
    """
    output_path = Path(output_dir) / character_name
    output_path.mkdir(parents=True, exist_ok=True)

    metadata = []
    valid_extensions = {'.png', '.jpg', '.jpeg', '.webp'}

    for i, img_path in enumerate(sorted(Path(image_dir).iterdir())):
        if img_path.suffix.lower() not in valid_extensions:
            continue

        # 打开并检查图片
        img = Image.open(img_path).convert('RGB')

        # 统一分辨率到 512x512(保持宽高比下填充)
        img.thumbnail((512, 512), Image.LANCZOS)
        new_img = Image.new('RGB', (512, 512), (255, 255, 255))
        offset = ((512 - img.width) // 2, (512 - img.height) // 2)
        new_img.paste(img, offset)

        # 保存处理后的图片
        new_name = f"{i+1:03d}.png"
        new_img.save(output_path / new_name)

        # 生成 BLIP 标注(简化版,实际建议用 BLIP 模型)
        caption = f"a portrait of {character_name}, detailed face, anime style"

        # 写入标注文件
        with open(output_path / f"{i+1:03d}.txt", 'w') as f:
            f.write(caption)

        metadata.append({
            "file_name": new_name,
            "text": caption,
            "character": character_name
        })

    # 写入 metadata
    with open(output_path / "metadata.jsonl", 'w') as f:
        for item in metadata:
            f.write(json.dumps(item, ensure_ascii=False) + 'n')

    print(f"✅ 数据集准备完成:共 {len(metadata)} 张图片")
    print(f"   输出路径: {output_path}")

if __name__ == "__main__":
    prepare_dataset("lisa_manga", "./raw_images/lisa", "./dataset")

步骤 3:训练配置

使用 Diffusers 训练 LoRA 的完整配置:

# train_lora.py — 完整 LoRA 训练脚本
import torch
from diffusers import StableDiffusionXLPipeline, AutoencoderKL
from diffusers import DDPMScheduler
from diffusers.optimization import get_scheduler
from diffusers.utils import convert_state_dict_to_diffusers
from peft import LoraConfig, get_peft_model_state_dict
from datasets import load_dataset
from torch.utils.data import DataLoader
from torchvision import transforms
import argparse
import os

def train_lora(
    pretrained_model_name="sd-models/realisticVisionV60B1",
    dataset_path="./dataset/lisa_manga",
    output_dir="./lora_models",
    lora_rank=64,
    learning_rate=1e-4,
    train_batch_size=1,
    max_train_steps=1500,
    save_every_n_steps=500,
):
    """训练漫画角色 LoRA 模型"""

    # 1. 加载预训练模型
    noise_scheduler = DDPMScheduler.from_pretrained(
        pretrained_model_name, subfolder="scheduler"
    )
    pipe = StableDiffusionXLPipeline.from_pretrained(
        pretrained_model_name,
        torch_dtype=torch.float16,
        variant="fp16",
    )
    vae = AutoencoderKL.from_pretrained(
        "madebyollin/sdxl-vae-fp16-fix",
        torch_dtype=torch.float16,
    )

    # 2. 冻结 VAE 和 UNet 的主干
    vae.requires_grad_(False)
    pipe.unet.requires_grad_(False)

    # 3. 配置 LoRA
    lora_config = LoraConfig(
        r=lora_rank,
        lora_alpha=lora_rank // 2,
        target_modules=["to_q", "to_k", "to_v", "to_out.0"],
        lora_dropout=0.1,
        bias="none",
    )
    pipe.unet.add_adapter(lora_config)

    # 4. 加载数据集
    dataset = load_dataset("imagefolder", data_dir=dataset_path, split="train")

    train_transforms = transforms.Compose([
        transforms.Resize(512, interpolation=transforms.InterpolationMode.BILINEAR),
        transforms.CenterCrop(512),
        transforms.ToTensor(),
        transforms.Normalize([0.5], [0.5]),
    ])

    def preprocess(examples):
        images = [train_transforms(img.convert("RGB")) for img in examples["image"]]
        return {"pixel_values": images, "caption": examples["text"]}

    dataset = dataset.with_transform(preprocess)
    train_dataloader = DataLoader(
        dataset, batch_size=train_batch_size, shuffle=True
    )

    # 5. 优化器设置
    optimizer = torch.optim.AdamW(
        pipe.unet.parameters(), lr=learning_rate
    )
    lr_scheduler = get_scheduler(
        "cosine",
        optimizer=optimizer,
        num_warmup_steps=100,
        num_training_steps=max_train_steps,
    )

    # 6. 训练循环
    pipe.unet.train()
    global_step = 0

    for epoch in range(100):  # 外层循环,由 max_train_steps 控制退出
        for batch in train_dataloader:
            if global_step >= max_train_steps:
                break

            # 编码潜空间
            latents = vae.encode(
                batch["pixel_values"].to("cuda", dtype=torch.float16)
            ).latent_dist.sample() * 0.18215

            # 添加噪声
            noise = torch.randn_like(latents)
            timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps,
                                      (latents.shape[0],), device="cuda")
            noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)

            # 预测噪声
            noise_pred = pipe.unet(noisy_latents, timesteps,
                                   encoder_hidden_states=pipe.text_encoder(
                                       batch["caption"] * 77  # 简化处理
                                   )[0]).sample

            # 计算损失
            loss = torch.nn.functional.mse_loss(noise_pred.float(), noise.float())

            # 反向传播
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            lr_scheduler.step()

            if global_step % 50 == 0:
                print(f"Step {global_step}/{max_train_steps} | Loss: {loss.item():.6f}")

            global_step += 1

        if global_step >= max_train_steps:
            break

    # 7. 保存 LoRA 权重
    os.makedirs(output_dir, exist_ok=True)
    unet_lora_state_dict = get_peft_model_state_dict(pipe.unet)
    output_path = os.path.join(output_dir, "character_lora.safetensors")
    torch.save(
        {"lora": convert_state_dict_to_diffusers(unet_lora_state_dict)},
        output_path,
    )
    print(f"✅ LoRA 模型已保存: {output_path}")

if __name__ == "__main__":
    train_lora()

步骤 4:执行训练

# 运行训练
python train_lora.py 
  --pretrained_model_name "runwayml/stable-diffusion-v1-5" 
  --dataset_path "./dataset/lisa_manga" 
  --output_dir "./lora_models" 
  --lora_rank 64 
  --learning_rate 1e-4 
  --train_batch_size 1 
  --max_train_steps 1500

# 训练完成后检查输出
ls -lh ./lora_models/
# 预期输出: character_lora.safetensors (约 50-150MB)

步骤 5:在推理中使用 LoRA

训练完成后,在 AI 漫画生成工作流中加载 LoRA:

# inference_with_lora.py — 使用 LoRA 生成漫画分镜
import torch
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
import os

# 加载基础模型 + LoRA
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    safety_checker=None,
)
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe = pipe.to("cuda")

# 加载 LoRA 权重
lora_path = "./lora_models/character_lora.safetensors"
pipe.load_lora_weights(lora_path, adapter_name="character")

# 生成不同分镜的多张图片
scenes = [
    "a portrait of <lora:character:1.0> lisa, close-up, cheerful expression, manga style",
    "a portrait of <lora:character:1.0> lisa, side view, serious look, anime shading",
    "a portrait of <lora:character:1.0> lisa, full body, standing under cherry blossom tree",
    "a portrait of <lora:character:1.0> lisa, action pose, dynamic angle, manga panel",
]

for i, prompt in enumerate(scenes):
    image = pipe(
        prompt=prompt,
        negative_prompt="low quality, distorted face, bad anatomy, extra fingers",
        num_inference_steps=30,
        guidance_scale=7.5,
        width=512,
        height=768,
    ).images[0]

    os.makedirs("./output", exist_ok=True)
    image.save(f"./output/scene_{i+1:02d}.png")
    print(f"✅ 生成: scene_{i+1:02d}.png — {prompt[:40]}...")

步骤 6:使用 Kohya_ss GUI(推荐新手)

Kohya_ss 提供了图形化界面,无需编写代码:

# 安装 Kohya_ss
git clone https://github.com/bmaltais/kohya_ss.git
cd kohya_ss
python setup.py

# 启动 GUI
python kohya_gui.py --listen 0.0.0.0 --port 7860

# 浏览器访问 http://localhost:7860
# 在 "LoRA" 标签页中配置:
# 1. 基础模型路径
# 2. 数据集文件夹路径
# 3. 训练参数(建议: Dim=64, LR=1e-4, Steps=1500)
# 4. 点击 "Start training"

数据集质量检查清单

{
  "dataset_quality_checklist": {
    "image_count": {
      "min": 20,
      "recommended": "50-100",
      "note": "少于 20 张难以收敛,超过 200 张边际效益递减"
    },
    "image_diversity": {
      "angles": ["正面", "3/4侧面", "正侧面", "俯视", "仰视"],
      "expressions": ["开心", "严肃", "惊讶", "悲伤", "愤怒"],
      "compositions": ["特写", "半身", "全身"],
      "note": "每个角度/表情至少 3-5 张"
    },
    "image_quality": {
      "resolution": "512x512 或更高",
      "format": "PNG (无损)",
      "background": "尽量干净背景,或使用透明背景",
      "note": "模糊、低对比度、光照过曝的图片应剔除"
    },
    "captioning": {
      "format": "每张图片配套 .txt 文件",
      "content": "描述角色特征、表情、动作、背景",
      "trigger_word": "使用统一的触发词如 'lisa_manga'",
      "example": "lisa_manga, a young girl with red hair, smiling, school uniform, front view"
    }
  }
}

常见问题

Q1:训练 LoRA 需要多少张图片?
最少 20 张高质量图片,推荐 50-100 张。图片质量比数量更重要——10 张高质量图片胜过 100 张低质量图片。图片应覆盖不同角度、表情和构图。

Q2:训练时出现过拟合(overfitting)怎么办?
过拟合表现为 LoRA 只能生成训练集中见过的角度,无法泛化。解决方案:① 增加训练图片数量 ② 降低 rank 值(32→16)③ 增大 dropout(0.1→0.2)④ 减少训练步数 ⑤ 增加数据增强(随机裁剪、翻转、色彩抖动)。

Q3:LoRA 权重应该设置多少?
在推理时通过 <lora:name:weight> 控制权重,默认为 1.0。建议范围 0.5-1.5。权重过高会导致角色特征过度而破坏构图,权重过低则角色特征不明显。实践中先从 0.8 开始,逐步调整。

Q4:多角色 LoRA 如何叠加?
Stable Diffusion WebUI 和 ComfyUI 都支持加载多个 LoRA。例如同时加载 lisa_mangatom_manga 两个 LoRA,在提示词中分别引用 <lora:lisa_manga:1.0><lora:tom_manga:1.0>。注意总权重和不宜超过 2.0,否则可能导致画面混乱。

Q5:训练时 VRAM 不足怎么办?
使用 --enable_xformers 启用 xformers 内存优化,将 batch size 设为 1,使用 gradient checkpointing,降低分辨率到 384×384 先测试。如果仍不足,考虑使用 LoRA 训练的”梯度累积”(gradient accumulation)或使用云端 GPU(AutoDL、Google Colab 等)。

总结

LoRA 训练是 AI 漫画创作者打造专属角色库的核心技能。掌握以下要点,你就能像传统漫画家设计角色一样,用 AI 精确控制每一个角色的视觉特征:

  1. 数据集质量决定一切:50-100 张覆盖多角度、多表情的高质量图片,加上精准的标注,是 LoRA 成功的基础
  2. 参数调优是艺术:rank 64 + LR 1e-4 + 1500 steps 是通用起点,但每个角色和风格都需要微调
  3. 多 LoRA 叠加实现多角色控制:可以在同一场景中通过不同的 LoRA 权重控制多个角色,实现漫画般的多人互动画面
  4. 训练与推理分离:一次训练,无限次使用——训练好的 LoRA 文件可以分享、复用,并嵌入到完整的漫画工作流中
  5. 持续迭代:随着角色在不同漫画中出现,可以不断补充训练数据,生成更高质量的 LoRA 版本

下一篇文章将介绍 IP-Adapter 实现角色一致性——一种无需训练、即插即用的角色控制方案,与 LoRA 形成完美的互补关系。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容