Diffusion 
扩散原理 
生成模型的目标是:给定一组数据,构建一个分布,生成新的数据
在物理学中很多微观过程都是时间可逆的,如果能知道当前系统的状态,理论上可以求出上一时间的状态。受此启发,如果我们知道从一幅画上如何一步步加噪声,也许能学会如何从噪声出发一步步去噪声得到一幅画。
扩散模型是一类概率生成模型,定义了两个马尔可夫过程:
前向过程:一个固定的马尔可夫链,将数据分布逐步添加高斯噪声,转为一种已知的先验分布(通常为标准高斯分布) 
反向过程:一个参数化的马尔可夫链,从先验分布开始逐步降噪,最终生成数据分布的样本 
 
扩散模型将一个复杂的抽样转为一系列的简单抽样,简化了学习
高斯扩散 
$$ 
x_{t+1}=x_t + \eta_t ,\quad \eta_t \sim N(0, \sigma^2) 
$$
一个假设:反向过程中,每一次去噪声的结果,仍是高斯分布。
于是这个分布可以用一组均值和方差表示,神经网络只需要预测这组参数,就能还原出$x_0$
DDPM 
去噪扩散概率模型(Denoising Diffusion Probabilistic Model)
 
DDPM,一个常用的构建反向采样器的方法,将复杂的变分推断问题简化为一个简单的去噪任务
DDPM的简化:
固定前向过程,前向加噪时使用一个预先设定的固定方差的高斯噪声,无需学习,只需要专注学习反向过程 
简化反向过程,反向去噪时每次都是用固定方差的高斯噪声,模型只需要学习预测高斯分布的均值 
重参数化目标,从学习预测图像转为预测噪声,极大地稳定了训练过程 
 
DDPM训练伪代码 
从数据集中随机抽取一批原始图像 $x_0$
 
为该批次中的每个图像随机选择一个时间步 $t$ (从1到T)
 
从标准正态分布中采样一个噪声 $\epsilon$
 
使用闭式解计算 $t$ 时刻的噪声图像 $x_t$
 
 
$$ 
x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon 
$$
将 $x_t$ 和 $t$ 输入到神经网络 中,得到预测的噪声 $\epsilon_{pred}$
 
计算损失: $loss = \text{MSE}(\epsilon, \epsilon_{pred})$
 
使用梯度下降更新模型参数 $\theta$
 
 
重参数化 
Reparameterization
 
对一个概率分布直接采样,是随机且不可导的。重参数化的核心是将采样分为两步:
从一个无参数的分布(如标准正态分布)中采样一个噪声变量 $\epsilon$
 
通过一个确定的可导的函数 $g_{\theta}(\epsilon)$ 将噪声转化为目标样本
 
 
于是采样过程变成了一个与参数 $\theta$ 有关的确定性函数
DDIM 
想象一下这样的场景:你站在山顶(数据分布),想要到达山谷(噪声分布)。DDPM告诉你必须沿着一条特定的蜿蜒小路走下去,每一步都要随机摇摆。而DDIM发现,实际上存在无数条路径可以到达同一个山谷,其中一些路径是完全笔直的!
 
DDPM的局限性:
采样的随机性,每一次去噪都会注入随机性,即使初始条件相同,也会得到不同的图像,无法精确控制生成过程 
步数依赖:DDPM需要1000步的去噪过程,减少步数会大幅降低生成质量 
不可逆:给定一张图片,无法还原出初始噪声,限制了图像编辑等模型的开发 
 
DDIM(Denoising Diffusion Implicit Models)提出了一种非马尔可夫采样过程,在保持相同训练目标的情况下,大幅减少采样次数(变为5~10次),并且生成更平滑更有确定性的样本
相关概念 
马尔可夫过程:核心特征是无记忆性,系统的未来状态仅于当前状态有关,而与历史状态(先前的状态)无关
最优传输理论:一种研究如何以最小成本将一个概率分布转移为另一种概率分布
对数似然下界(ELBO):用于求一个复杂概率分布的后验分布,由于直接计算后验分布十分困难,所以引入变分推断来近似
信噪比(SNR):信息/噪声
生成策略与具体样本 
为什么扩散模型不是“记住具体样本(instance)”,而是学会一种“生成策略(policy)”?
从训练目标来看:扩散模型优化的是分布,而非样本,不是学习某个具体的图像A,而是学习如果生成类似A的图像 
从采样方式来看:扩散是一个多步决策序列 
从泛化性角度,如果模型是instance base,那么采样时只会复现训练数据,对那些未训练的数据“不公平”(分布外泛化不公平) 
 
U-Net 
U-Net最初是一个医学图像分割模型,当时的需求是神经网络既要理解全局语义信息(这是什么器官),又要精确定位像素。传统的Encoder-Decoder架构当特征图被压缩到最小时,大量空间信息被丢失了,而U-Net通过引入跳跃连接(下图灰色箭头),让高分辨率的信息直接拷贝到后续部分。
U型的网络传递全局语义信息,跳跃连接传递局部高分辨像素信息
U-Net的优点:
对称性的架构,能保证输出和输入在空间上严格对齐 
多尺度的处理能力,在扩散过程中,前期高噪声时主要进行全局结构和语义重建,后期需要进行精修细节和清晰度,U-Net可以兼顾全局和局部 
高效,大部分复杂计算(如自注意力)在低分辨率的特征图中,在高分辨率下只做简单操作 
 
基于分数的生成模型 
从数学(Energy-Based)的角度,扩散模型本质上上学习数据的分数函数(score function)
分数函数 
给定概率密度函数$p(x)$,分数函数为 
$$ 
s(x) = \nabla_{x} \log p(x) 
$$ 
对数 概率密度 关于x的梯度
分数函数表示任意一点概率密度增长最快的方向,定义了数据流形上的向量场,这个局部信息可以指导我们如何在概率空间中“导航”
CFG 
Classifier-Free Guidance
 
早期Diffusion在无条件生成领域取得非常好的成果,然而条件生成却很一般(没能超越GAN),通过直接在噪声预测网络中加条件,可以保证多样性,但生成的内容模糊、保真度低。朴素的条件生成无法实现低温度采样(不牺牲太多多样性,提高样本的锐度和符合条件的精确性)的原因,是噪声的随机性导致采样路径偏离条件生成的高密度区
分类器指导 是一种采样时干预技术,先训练一个条件生成的Diffusion,然后基于加入噪声的图片,训练一个噪声分类器。在推理时,分类器会在采样中注入梯度,使得采样朝着高条件似然方向倾斜,提升了样本的保真度,改善了条件遵守。
但是,额外训练一个分类器指导是十分麻烦的。CFG的作者认为分类器指导的本质,是通过隐式分类器实现的,而score的线性组合是可以等价于分类器的。使用一个单一的神经网络,同时训练条件生成和无条件生成。实现方式是在训练时随机丢弃一些条件信息(0.1~0.2的概率),模型会同时掌握条件和无条件生成。
ViT 
ViT将Transformer架构引入到CV领域,核心思想是将输入图片切分为一个个小patch,为这些patch标注类别token,将这些pathc和类别一同送入Transformer中,生成分类
DiT的输入是一个个带噪声的图像patch和条件token(如位置和时间步数),送入Transformer后得到输出token,解码得到每个patch对应的噪声
Flow Matching 
扩散模型将一个复杂的抽样转为一系列的简单抽样,传统的UNet通过预测噪声得到最终样本,这个过程是不确定的。但流模型将简单分布转为负责分布的过程是一个确定的可逆过程,给定初始值和参数,流模型的输出结果是确定的
而这个可逆过程,一般是由一个ODE定义的 
$$ 
\frac{dz}{dt}=f(z, t, \theta) 
$$
Flow Matching的噪声公式:
$$ 
x_{t-\Delta t}=x_t - v_t \Delta_{t} 
$$ 
相较于DDPM和DDIM,Flow Matching更简洁、直观、可逆
DreamBooth 
目前huggingface diffusers库提供的Flux训练均基于DreamBooth,其核心机制是:
稀有标识符:在instance prompt中插入一些token,如 a dog 变成 a [V]dog,使得这个词会变得很稀有,训练效果会更好 
保留先验损失:为了避免语义漂移,如将所有的 a dog 都绑定为 a [V]dog,会用训练前的模型用 a dog  生成出一些样本加入训练集中,这样模型能理解两者的差异 
 
在使用huggingface 的DreamBooth训练脚本训练时,如果关闭了textencoder训练、关闭了稀有标识符替换、关闭了先验保留,就跟普通的sft没有区别了。感觉社区还是更喜欢普通sft,这种需要替换textencoder的模型还是太重了
 
应用 
如何组织 prompt 
T2I 
from  diffusers import  DiffusionPipelinepipe_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  DiffusionPipelinepipe_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  torchfrom  PIL import  Imagefrom  diffusers import  StableDiffusionXLImg2ImgPipelinepipe_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        ).images[0 ] output_image.save(f"outputs/1.jpg" ) 
 
I2I LoRA Controlnet 
直接使用I2I LoRA效果并不好,对原图的控制能力比较弱,可以配合使用Controlnet使用
import  osimport  cv2import  torchimport  numpy as  npfrom  PIL import  Imagefrom  diffusers import  StableDiffusionXLControlNetImg2ImgPipeline, ControlNetModeloutput_folder = "outputs"  os.makedirs(output_folder, exist_ok=True ) 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) 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_sdxlprompt = "..."  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  torchfrom  diffusers import  AutoencoderKLfrom  PIL import  Imageimport  numpy as  npvae = 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" ) image = image.resize((1024 , 1024 )) 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 )   img_tensor = img_tensor.to(device, dtype=vae.dtype) with  torch.no_grad():    latent_dist = vae.encode(img_tensor)          latents = latent_dist.latent_dist.mode()          latents = latents * vae.config.scaling_factor + vae.config.shift_factor with  torch.no_grad():         latents_for_decode = latents / vae.config.scaling_factor + vae.config.shift_factor     decoded = vae.decode(latents_for_decode).sample 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 
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  
 
import  osimport  hashlibimport  argparseimport  torchfrom  diffusers import  AutoPipelineForText2Imagefrom  safetensors.torch import  load_fileif  __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()          model = AutoPipelineForText2Image.from_pretrained(args.model_id, torch_dtype=torch.bfloat16).to("cuda" )          model.load_lora_weights(args.lora_name, weight_name="pytorch_lora_weights.safetensors" )     model.set_adapters("default_0" , args.lora_scale)           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》
扩散模型教程