首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >AIGC 摄影训练工程化:从打标数据到可控出图的完整管线

AIGC 摄影训练工程化:从打标数据到可控出图的完整管线

原创
作者头像
用户12339161
发布于 2026-10-14 16:47:10
发布于 2026-10-14 16:47:10
320
举报

核心结论: AIGC 摄影训练的关键不在"跑通一个 LoRA",而在数据标注质量与训练参数的匹配。同一批图、同一个底模,打标方式不同、学习率不同,出图的人脸稳定性和光影真实度能差出一个档次。工程要做的,是把打标、训练、评估三步固化成可复现的管线,而不是每次靠手感调参。

一、AIGC 摄影与传统修图的区别

传统摄影后期是"调整已有像素",AIGC 摄影是"生成不存在的光影"。前者的可控性来自参数,后者的可控性来自两个东西:训练数据的分布和提示词的约束。

一个常见的失败案例:拿 30 张人像去训 LoRA,出图时背景总是一团模糊。原因不是模型不行,是训练集里 28 张都是室内浅景深,模型把"背景模糊"当成了这个人的固有属性。训练集的偏差会被模型完整继承。

二、数据层:打标决定上限

训练前先做三件事:清洗、裁剪、打标。打标是最容易被敷衍的一步,也是影响最大的。

代码语言:javascript
复制
import os
from PIL import Image
from transformers import BlipProcessor, BlipForConditionalGeneration

processor = BlipProcessor.from_pretrained(
    "Salesforce/blip-image-captioning-base")
model = BlipForConditionalGeneration.from_pretrained(
    "Salesforce/blip-image-captioning-base")

def clean_and_resize(src_dir: str, dst_dir: str,
                     size: int = 1024) -> list:
    """统一分辨率、去重、剔除低质量图"""
    os.makedirs(dst_dir, exist_ok=True)
    kept = []
    seen_hashes = set()
    for name in os.listdir(src_dir):
        if not name.lower().endswith((".jpg", ".png")):
            continue
        path = os.path.join(src_dir, name)
        img = Image.open(path).convert("RGB")
        # 短边不足直接剔除,避免放大失真
        if min(img.size) < 512:
            continue
        h = hash(img.resize((8, 8)).tobytes())
        if h in seen_hashes:
            continue
        seen_hashes.add(h)
        img = img.resize((size, size), Image.LANCZOS)
        out = os.path.join(dst_dir, name)
        img.save(out, quality=95)
        kept.append(out)
    return kept

def auto_caption(image_path: str, trigger: str) -> str:
    """生成基础描述,人工再修正"""
    raw = Image.open(image_path).convert("RGB")
    inputs = processor(raw, return_tensors="pt")
    out = model.generate(**inputs, max_new_tokens=50)
    text = processor.decode(out[0], skip_special_tokens=True)
    return f"{trigger}, {text}"

关键工程细节:触发词(trigger)必须唯一。用 ohwx 这种无意义字符串,避免与底模已有概念冲突。打标时只描述"变化的属性"(服装、角度、光线),不描述"固定的属性"(这个人本身),否则模型会把固定属性也当成变量。

三、训练层:LoRA 参数配置

以 diffusers 的 LoRA 训练为例,核心参数如下:

代码语言:javascript
复制
from diffusers import StableDiffusionPipeline, DDPMScheduler
from peft import LoraConfig, get_peft_model
import torch

pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16).to("cuda")
unet = pipe.unet

lora_config = LoraConfig(
    r=16,                      # 秩:8-32 之间,人像常用 16
    lora_alpha=32,             # 缩放:通常为 r 的 2 倍
    target_modules=["to_q", "to_k", "to_v", "to_out.0"],
    lora_dropout=0.05,
    bias="none")
unet = get_peft_model(unet, lora_config)

optimizer = torch.optim.AdamW(
    unet.parameters(), lr=1e-4, weight_decay=1e-2)
scheduler = DDPMScheduler.from_pretrained(
    "runwayml/stable-diffusion-v1-5", subfolder="scheduler")

三个参数决定成败。学习率 1e-4 是 LoRA 的常用起点,调到 5e-4 容易过拟合,人脸会僵化;调到 5e-5 训练不足,出图不像。rank=16 对单人人像够用,训多人或复杂场景要提到 32。lora_alpha=32 是 rank 的两倍,控制 LoRA 权重对原模型的影响强度。

训练循环的损失计算:

代码语言:javascript
复制
def train_step(batch, unet, vae, text_encoder, tokenizer,
               noise_scheduler, device="cuda"):
    images = batch["pixel_values"].to(device)
    captions = batch["captions"]

    latents = vae.encode(images).latent_dist.sample() * 0.18215
    noise = torch.randn_like(latents)
    timesteps = torch.randint(
        0, noise_scheduler.config.num_train_timesteps,
        (latents.shape[0],), device=device).long()
    noisy = noise_scheduler.add_noise(latents, noise, timesteps)

    tokens = tokenizer(
        captions, padding="max_length",
        max_length=tokenizer.model_max_length,
        truncation=True, return_tensors="pt").to(device)
    encoder_hidden = text_encoder(tokens.input_ids)[0]

    pred = unet(noisy, timesteps, encoder_hidden).sample
    loss = torch.nn.functional.mse_loss(pred, noise)
    loss.backward()
    return loss.item()

四、评估层:用固定提示词对比

训练完不能只看几张图觉得"像"。要固定一组提示词,对比训练前后的输出差异:

代码语言:javascript
复制
def evaluate(pipe, lora_path: str, prompts: list,
             seed: int = 42) -> dict:
    """固定 seed 和提示词,出图用于横向对比"""
    pipe.unload_lora_weights()
    pipe.load_lora_weights(lora_path)
    results = {}
    generator = torch.Generator("cuda").manual_seed(seed)
    for p in prompts:
        img = pipe(p, num_inference_steps=30,
                   guidance_scale=7.5,
                   generator=generator).images[0]
        results[p] = img
    return results

eval_prompts = [
    "ohwx person, close-up portrait, soft window light",
    "ohwx person, full body, outdoor, golden hour",
    "ohwx person, side profile, studio lighting, neutral background",
]

评估看三个维度:人脸一致性(多提示词下是不是同一个人)、光影真实度(有没有塑料感)、背景可控性(换背景提示词时背景是否真的变了)。如果背景不跟着变,说明训练集背景太单一,模型把它当成了触发词的一部分。

五、工程化要点

第一,打标前先做分布检查。 统计训练集的背景、光线、角度分布。某一类占比超过 70%,模型就会把它当成固有属性。

第二,触发词用无意义字符串。 ohwx、sks 这类不与已有概念冲突的 token,比用真人名字更干净。

第三,学习率宁低勿高。 过拟合的 LoRA 出图千人一面,无法调整。欠拟合还能靠提示词补,过拟合基本没救。

第四,保存中间 checkpoint。 每 500 步存一次,训完横向对比,选泛化最好的那个,而不是最后一个。

第五,评估必须固定 seed。 换 seed 对比等于没对比,变量太多看不出 LoRA 本身的效果。

AIGC 摄影训练不是"喂图等结果",而是用数据分布控制模型认知的过程。打标决定模型学什么,参数决定学多深,评估决定选哪一个。三步都固化成流程,出图才可复现。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、AIGC 摄影与传统修图的区别
  • 二、数据层:打标决定上限
  • 三、训练层:LoRA 参数配置
  • 四、评估层:用固定提示词对比
  • 五、工程化要点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档