Diffusion
扩散原理
生成模型的目标是:给定一组数据,构建一个分布,生成新的数据
在物理学中很多微观过程都是时间可逆的,如果能知道当前系统的状态,理论上可以求出上一时间的状态。受此启发,如果我们知道从一幅画上如何一步步加噪声,也许能学会如何从噪声出发一步步去噪声得到一幅画。
扩散模型是一类概率生成模型,定义了两个马尔可夫过程:
前向过程:一个固定的马尔可夫链,将数据分布逐步添加高斯噪声,转为一种已知的先验分布(通常为标准高斯分布)
反向过程:一个参数化的马尔可夫链,从先验分布开始逐步降噪,最终生成数据分布的样本
扩散模型将一个复杂的抽样转为一系列的简单抽样,简化了学习
高斯扩散
$$
x_{t+1}=x_t + \eta_t ,\quad \eta_t \sim N(0, \sigma^2)
$$
一个假设:反向过程中,每一次去噪声的结果,仍是高斯分布。
于是这个分布可以用一组均值和方差表示,神经网络只需要预测这组参数,就能还原出$x_0$
DDPM
去噪扩散概率模型
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$ 有关的确定性函数
相关概念
马尔可夫过程:核心特征是无记忆性,系统的未来状态仅于当前状态有关,而与历史状态(先前的状态)无关
最优传输理论:一种研究如何以最小成本将一个概率分布转移为另一种概率分布
对数似然下界(ELBO):用于求一个复杂概率分布的后验分布,由于直接计算后验分布十分困难,所以引入变分推断来近似
信噪比(SNR):信息/噪声
U-Net
U-Net最初是一个医学图像分割模型,当时的需求是神经网络既要理解全局语义信息(这是什么器官),又要精确定位像素。传统的Encoder-Decoder架构当特征图被压缩到最小时,大量空间信息被丢失了,而U-Net通过引入跳跃连接(下图灰色箭头),让高分辨率的信息直接拷贝到后续部分。
U型的网络传递全局语义信息,跳跃连接传递局部高分辨像素信息
U-Net的优点:
对称性的架构,能保证输出和输入在空间上严格对齐
多尺度的处理能力,在扩散过程中,前期高噪声时主要进行全局结构和语义重建,后期需要进行精修细节和清晰度,U-Net可以兼顾全局和局部
高效,大部分复杂计算(如自注意力)在低分辨率的特征图中,在高分辨率下只做简单操作
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 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》
扩散模型教程