抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Diffusion

扩散原理

生成模型的目标是:给定一组数据,构建一个分布,生成新的数据

在物理学中很多微观过程都是时间可逆的,如果能知道当前系统的状态,理论上可以求出上一时间的状态。受此启发,如果我们知道从一幅画上如何一步步加噪声,也许能学会如何从噪声出发一步步去噪声得到一幅画。

扩散模型是一类概率生成模型,定义了两个马尔可夫过程:

  1. 前向过程:一个固定的马尔可夫链,将数据分布逐步添加高斯噪声,转为一种已知的先验分布(通常为标准高斯分布)
  2. 反向过程:一个参数化的马尔可夫链,从先验分布开始逐步降噪,最终生成数据分布的样本

扩散模型将一个复杂的抽样转为一系列的简单抽样,简化了学习

高斯扩散

$$
x_{t+1}=x_t + \eta_t ,\quad \eta_t \sim N(0, \sigma^2)
$$

一个假设:反向过程中,每一次去噪声的结果,仍是高斯分布。

于是这个分布可以用一组均值和方差表示,神经网络只需要预测这组参数,就能还原出$x_0$

DDPM

去噪扩散概率模型

DDPM,一个常用的构建反向采样器的方法,将复杂的变分推断问题简化为一个简单的去噪任务

DDPM的简化:

  1. 固定前向过程,前向加噪时使用一个预先设定的固定方差的高斯噪声,无需学习,只需要专注学习反向过程
  2. 简化反向过程,反向去噪时每次都是用固定方差的高斯噪声,模型只需要学习预测高斯分布的均值
  3. 重参数化目标,从学习预测图像转为预测噪声,极大地稳定了训练过程

DDPM训练伪代码

  1. 从数据集中随机抽取一批原始图像 $x_0$

  2. 为该批次中的每个图像随机选择一个时间步 $t$ (从1到T)

  3. 从标准正态分布中采样一个噪声 $\epsilon$

  4. 使用闭式解计算 $t$ 时刻的噪声图像 $x_t$

$$
x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon
$$

  1. 将 $x_t$ 和 $t$ 输入到神经网络 中,得到预测的噪声 $\epsilon_{pred}$

  2. 计算损失: $loss = \text{MSE}(\epsilon, \epsilon_{pred})$

  3. 使用梯度下降更新模型参数 $\theta$

重参数化

Reparameterization

对一个概率分布直接采样,是随机且不可导的。重参数化的核心是将采样分为两步:

  1. 从一个无参数的分布(如标准正态分布)中采样一个噪声变量 $\epsilon$

  2. 通过一个确定的可导的函数 $g_{\theta}(\epsilon)$ 将噪声转化为目标样本

于是采样过程变成了一个与参数 $\theta$ 有关的确定性函数

相关概念

马尔可夫过程:核心特征是无记忆性,系统的未来状态仅于当前状态有关,而与历史状态(先前的状态)无关

最优传输理论:一种研究如何以最小成本将一个概率分布转移为另一种概率分布

对数似然下界(ELBO):用于求一个复杂概率分布的后验分布,由于直接计算后验分布十分困难,所以引入变分推断来近似

信噪比(SNR):信息/噪声

U-Net

U-Net最初是一个医学图像分割模型,当时的需求是神经网络既要理解全局语义信息(这是什么器官),又要精确定位像素。传统的Encoder-Decoder架构当特征图被压缩到最小时,大量空间信息被丢失了,而U-Net通过引入跳跃连接(下图灰色箭头),让高分辨率的信息直接拷贝到后续部分。

U型的网络传递全局语义信息,跳跃连接传递局部高分辨像素信息

UNet

U-Net的优点:

  1. 对称性的架构,能保证输出和输入在空间上严格对齐
  2. 多尺度的处理能力,在扩散过程中,前期高噪声时主要进行全局结构和语义重建,后期需要进行精修细节和清晰度,U-Net可以兼顾全局和局部
  3. 高效,大部分复杂计算(如自注意力)在低分辨率的特征图中,在高分辨率下只做简单操作

ViT

ViT将Transformer架构引入到CV领域,核心思想是将输入图片切分为一个个小patch,为这些patch标注类别token,将这些pathc和类别一同送入Transformer中,生成分类

DiT的输入是一个个带噪声的图像patch和条件token(如位置和时间步数),送入Transformer后得到输出token,解码得到每个patch对应的噪声

Flow Matching

向量场定义了一个ODE,这个ODE的解会形成一个轨迹,所有轨迹的集合就是Flow

应用

如何组织 prompt

T2I

from diffusers import DiffusionPipeline

pipe_id = "stabilityai/stable-diffusion-xl-base-1.0"
pipe = DiffusionPipeline.from_pretrained(pipe_id, torch_dtype=torch.float16).to("cuda")

prompt = "a blue hair gril"
image = pipe(prompt, num_inference_steps=45, guidance_scale=7.5, height=1024, width=1024).images[0]

image.save("output.jpg")

T2I LoRA

LoRA可以改变模型画风

from diffusers import DiffusionPipeline

pipe_id = "stabilityai/stable-diffusion-xl-base-1.0"
pipe = DiffusionPipeline.from_pretrained(pipe_id, torch_dtype=torch.float16).to("cuda")
pipe.load_lora_weights("sd-gbf-lora", weight_name="default_0")
pipeline.set_adapters("default_0", 1.0)

prompt = "a blue hair gril"
lora_scale = 0.9
image = pipe(prompt, num_inference_steps=45, guidance_scale=7.5, cross_attention_kwargs={"scale": lora_scale}, height=1024, width=1024).images[0]

image.save("output.jpg")

I2I LoRA

将图片转为LoRA画风

import torch
from PIL import Image
from diffusers import StableDiffusionXLImg2ImgPipeline

pipe_id = "stabilityai/stable-diffusion-xl-base-1.0"
pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(pipe_id, torch_dtype=torch.float16).to("cuda")
pipe.load_lora_weights("sd-gbf-lora")

# 加载输入图像
input_image_path = "examples/lubi.jpg"
input_image = Image.open(input_image_path).convert("RGB")

prompt = "gbfhero"
negative_prompt = "low quality, bad quality"

with torch.no_grad():
output_image = pipe(
prompt=prompt,
negative_prompt=negative_prompt,
guidance_scale=7.5,
cross_attention_kwargs={"scale": 0.9},
height=1024, width=1024,
image=input_image,
strength=0.5 # 控制生成图像与输入图像的相似程度,范围为0到1
).images[0]

output_image.save(f"outputs/1.jpg")

I2I LoRA Controlnet

直接使用I2I LoRA效果并不好,对原图的控制能力比较弱,可以配合使用Controlnet使用

import os
import cv2
import torch
import numpy as np
from PIL import Image
from diffusers import StableDiffusionXLControlNetImg2ImgPipeline, ControlNetModel

output_folder = "outputs"
os.makedirs(output_folder, exist_ok=True)

# 加载模型和Controlnet
pipe_id = "stabilityai/stable-diffusion-xl-base-1.0"
controlnet_id = "diffusers/controlnet-canny-sdxl-1.0"
controlnet = ControlNetModel.from_pretrained(controlnet_id, torch_dtype=torch.float16)
pipe = StableDiffusionXLControlNetImg2ImgPipeline.from_pretrained(pipe_id, controlnet=controlnet, torch_dtype=torch.float16).to("cuda")
pipe.load_lora_weights("sd-gbf-lora3")

# 加载图片
input_image_path = "examples/leishen.jpeg"
input_image = Image.open(input_image_path).convert("RGB")
np_image = np.array(input_image)
# 生成 edges
np_image = cv2.Canny(np_image, 100, 200)
np_image = np_image[:, :, None]
np_image = np.concatenate([np_image, np_image, np_image], axis=2)
canny_image = Image.fromarray(np_image)
canny_image.save(f'{output_folder}/tmp_edge.png')

prompt = "gbfhero, clean background"
negative_prompt = "low quality, bad quality"
lora_scale = 0.9
image = pipe(prompt,
negative_prompt=negative_prompt,
guidance_scale=7.5,
cross_attention_kwargs={"scale": lora_scale},
controlnet_conditioning_scale=0.5,
image=input_image,
strength=0.9,
control_image=canny_image,
height=1024, width=1024).images[0]

image.save(f"{output_folder}/5.jpg")

更长的prompt

SD画图经常遇到CLIP能力限制,Token数不够的问题,这限制了我们使用更多更长的prompt

可以使用sd_embed库,克服77 Token限制

from sd_embed.embedding_funcs import get_weighted_text_embeddings_sdxl

prompt = "..."
negative_prompt = "..."
seed = 481167465

pipeline = StableDiffusionXLPipeline.from_single_file(
model_path,
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16",
scheduler = scheduler
).to("cuda")

(
prompt_embeds,
prompt_neg_embeds,
pooled_prompt_embeds,
negative_pooled_prompt_embeds
) = get_weighted_text_embeddings_sdxl(
pipeline,
prompt=prompt,
neg_prompt=negative_prompt
)

# 生成图像
image = pipeline(
prompt_embeds=prompt_embeds,
negative_prompt_embeds=prompt_neg_embeds,
pooled_prompt_embeds=pooled_prompt_embeds,
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
num_inference_steps=28,
guidance_scale=5.0,
generator=torch.Generator(device=pipeline.device).manual_seed(seed),
height=1216,
width=832
).images[0]

检查VAE

只加载vae,将图片编码为latent,再解码回图像,这里使用Flux的VAE

import torch
from diffusers import AutoencoderKL
from PIL import Image
import numpy as np

vae = AutoencoderKL.from_pretrained("black-forest-labs/FLUX.1-dev", subfolder="vae", torch_dtype=torch.bfloat16)
device = "cuda" if torch.cuda.is_available() else "cpu"
vae = vae.to(device)

image_path = "0.png"
image = Image.open(image_path).convert("RGB")

# Resize to a size divisible by 16 (e.g., 1024x1024 for Flux)
image = image.resize((1024, 1024))

# Preprocess image to tensor in [-1, 1]
img_array = np.array(image).astype(np.float32) / 255.0
img_array = img_array * 2.0 - 1.0
img_tensor = torch.from_numpy(img_array).permute(2, 0, 1).unsqueeze(0) # Shape: (1, 3, H, W)
img_tensor = img_tensor.to(device, dtype=vae.dtype)

# Encode the image to latents
with torch.no_grad():
latent_dist = vae.encode(img_tensor)
# Use mode (mean) for better reconstruction
latents = latent_dist.latent_dist.mode()
# Apply scale and shift (specific to Flux VAE)
latents = latents * vae.config.scaling_factor + vae.config.shift_factor

# Decode the latents back to image
with torch.no_grad():
# Reverse shift and scale
latents_for_decode = latents / vae.config.scaling_factor + vae.config.shift_factor
decoded = vae.decode(latents_for_decode).sample

# Postprocess decoded tensor to [0, 1] and convert to PIL image
decoded = (decoded + 1.0) / 2.0
decoded = torch.clamp(decoded, 0.0, 1.0)
decoded_img = (decoded[0].float().permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8)
output_image = Image.fromarray(decoded_img)

output_image.save("0-re.png")

训练

Flux DreamBooth LoRA

advanced_diffusion_training

基于代码train_dreambooth_lora_flux_advanced.py

# train.sh
export MODEL_NAME="black-forest-labs/FLUX.1-dev"
export DATASET_NAME="Reuben-Sun/ATDAN-pictures"
export DATASET_PATH="ATDAN-pictures"
export OUTPUT_DIR="Flux-LoRA-ATDAN"
export CUDA_VISIBLE_DEVICES='0,1,2,3'

accelerate launch \
--num_processes=4 \
--num_machines=1 \
--multi_gpu \
train_dreambooth_lora_flux_advanced.py \
--pretrained_model_name_or_path=$MODEL_NAME \
--instance_data_dir=$DATASET_PATH \
--instance_prompt="ATDAN" \
--validation_prompt="ATDAN, a vibrant blue and white anime girl with long white hair and a crown on her head. Her eyes are glowing with a yellow hue, adding a pop of color to the scene. Her hands are clasped in front of her face, while her hands are held together in a praying position. The background is dark, creating a stark contrast to the girl's outfit" \
--validation_epochs=20 \
--output_dir=$OUTPUT_DIR \
--mixed_precision="bf16" \
--resolution=1024 \
--train_batch_size=2 \
--repeats=1 \
--report_to="tensorboard"\
--gradient_accumulation_steps=1 \
--gradient_checkpointing \
--learning_rate=1e-4 \
--optimizer="adamW" \
--lr_scheduler="constant" \
--lr_warmup_steps=0 \
--rank=32 \
--lora_alpha=16 \
--max_train_steps=4000 \
--checkpointing_steps=200 \
--checkpoints_total_limit=10 \
--seed=42 \
--guidance_scale=1.0
# inference.py
import os
import hashlib
import argparse
import torch
from diffusers import AutoPipelineForText2Image
from safetensors.torch import load_file

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Inference script for Flux LoRA model")
parser.add_argument("--model_id", type=str, default='black-forest-labs/FLUX.1-dev')
parser.add_argument("--lora_name", type=str, required=True)
parser.add_argument('--lora_scale', type=float, default=1.0, help='Scale for LoRA weights')
parser.add_argument("--prompt", type=str, required=True)
parser.add_argument("--steps", type=int, default=25)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--guidance_scale", type=float, default=7.5)
args = parser.parse_args()

# Load the model
model = AutoPipelineForText2Image.from_pretrained(args.model_id, torch_dtype=torch.bfloat16).to("cuda")
# Load LoRA weights
model.load_lora_weights(args.lora_name, weight_name="pytorch_lora_weights.safetensors")
model.set_adapters("default_0", args.lora_scale)

# Inference
with torch.no_grad():
generator = torch.Generator(device=model.device).manual_seed(args.seed)
output_image = model(prompt=args.prompt, num_inference_steps=args.steps, guidance_scale=args.guidance_scale, generator=generator).images[0]

output_image.save('output.png')

参考

diffusion_tutorial

《Step-by-Step Diffusion: An Elementary Tutorial》

扩散模型教程

评论