IP Adapter,我愿称之它为SD垫图
IP Adapter是腾讯lab发布的一个新的Stable Diffusion适配器,它的作用是将你输入的图像作为图像提示词,本质上就像MJ的垫图。
IP Adapter比reference的效果要好,而且会快很多,适配于各种stable diffusion模型,还能和controlnet等一起用。
目前更新了faceID模型,专门用于人脸的
然后支持了stable diffusion xl,但是还没支持turbo模型
作者回复:这正在考虑中,但尚未实施(似乎 SDXL turbo 的训练代码尚未发布) 23.12
模型基础知识
主模型等通常16位就行,不带fp16的代表fp32模型,带来的效果提升不显著却占了很大空间。我们不用下载这些。
Diffusers库学习
来源:https://zhuanlan.zhihu.com/p/672574978
简介:
扩散模型相关的工具包,可以帮助推理,训练等。类似于pytorch开源的TorchVision库,不过这个库开源性,实时性,都更强。
目前很多研究论文都是直接基于Diffusers库开发他们的代码。
特色优点:
开源性强(一个论文都可以把自己模型相关的代码贡献进去)、
实效性强:最新的模型架构很快能得到工具支持,比如新出的xl模型,lcm加速,想用直接掉他的接口就行了,不用自己适配一遍各种配置,自己写个接口,再进行调用
接口简单,非常适合小白入门文生图、图生图等研究,同样也极大地方便了大佬们对代码的迭代优化。
官方介绍:
Diffusers 都是一个支持两者的模块化工具箱。我们的库在设计时注重可用性而不是性能,简单而不是容易,可定制性而不是抽象。
网友评价:Diffusers架构比较好,开发体验好,不过支持配置的东西没那么多。
我们直接找到典中典那五行代码
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(“runwayml/stable-diffusion-v1-5”, torch_dtype=torch.float16)
pipeline.to(“cuda”)
pipeline(“An image of a squirrel in Picasso style”).images[0]
运行这段代码它会自动替你下载,模型之又多又大、网速之慢(不fq的话),十分感人。
支持本地路径,可以自己下载保存,把url改成本地路径的url就行了,似乎还要添加 variant='fp16’的参数,不然无法识别fp16的模型。
既然是代码级讲解,我很有必要讲一讲这个DiffusionPipeline。如果你想要全面进入Diffusers的广阔天地,了解Diffusers库的核心Pipeline是一大关键。
Pipeline基本结构
Pipeline是Diffusers库的最大单位,一个pipeline就可以让你在五行之内实现文生图功能。由此可见,一个pipeline模块的代码量是相当庞大的。
事实上,一个pipeline包含了如下四大件(含大模型文件):
VAE,图像变到潜空间,或者反之。缩小图像,给Unet生成。或者反之。
UNet,CNN网络,噪声预测的部分
Text-Encoder,用于把tokens编码为一串向量,用来控制扩散模型的生成。他有附属组件 Tokenizer,把输入的文本按照字典编码为tokens
Scheduler,采样规划器,控制采样方法,采样过程的。
其它小模块:
Safety_checker,NSFW检测器,很多人应该都不想要这个(shide),后面我会讲怎么去掉这一部分
Feature_extractor,也是NSFW检测器的一部分,也可以去掉。
StableDiffusionPipeline的每一个模块都对应一个类,如VAE模块对应的是diffusers库中的AutoencoderKL类,所以说StableDiffusionPipeline类的初始化会同时完成七个子模块对应类的初始化。
Diffusers库中的大多数类我们首先就要看__init__()函数和__call()函数。和torch的model类似的。
模型下载相关
使用细节:
这个东西流量不要钱,如果模型发生了任何更新,包括readme,他就要重新下载一遍所有模型,有毒
这时候需要设置这个东西
revision= “86005d20dc90288067e881dc574607cdbf6b1d72”
后面填写模型所在目录的 snapshots 下的上次下载的快照名字,或者按照文档的commit id a branch name, tag name, a commit id, or any identifier by Git. 等,这个需要去原始仓库里面找,没试过。
内存优化
我们通过调用 enable_model_cpu_offload 函数来启用智能 CPU 卸载,而不是直接将 pipeline 加载到 GPU 上。
智能 CPU 卸载是一种降低显存占用的方法。扩散模型 (如 Stable Diffusion) 的推理并不是运行一个单独的模型,而是多个模型组件的串行推理。如在推理 ControlNet Stable Diffusion 时,需要首先运行 CLIP 文本编码器,其次推理扩散模型 UNet 和 ControlNet,然后运行 VAE 解码器,最后运行 safety checker (安全检查器,主要用于审核过滤违规图像)。而在扩散过程中大多数组件仅运行一次,因此不需要一直占用 GPU 内存。通过启用智能模型卸载,可以确保每个组件在不需要参与 GPU 计算时卸载到 CPU 上,从而显著降低显存占用,并且不会显著增加推理时间 (仅增加了模型在 GPU-CPU 之间的转移时间)。
注意: 启用 enable_model_cpu_offload 后,pipeline 会自动进行 GPU 内存管理,因此请不要再使用 .to(“cuda”) 手动将 pipeline 转移到 GPU。
model_id = “sd-dreambooth-library/mr-potato-head”
pipe = StableDiffusionControlNetPipeline.from_pretrained(
model_id,
controlnet=controlnet,
torch_dtype=torch.float16,
)
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config)
pipe.enable_model_cpu_offload()
pipe.enable_xformers_memory_efficient_attention()
diffusers的 StableDiffusionPipeline类学习
init
def init(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
scheduler: xxxSchedulers, #这个貌似经常变化,我一般用DDIMSchedulers
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPImageProcessor,
requires_safety_checker: bool = True,
):
super().init()
requires_safety_checker可以设置为False,可以跳过Safety_checker的检测
__call__函数
这段就是整个生成环节的代码:
def __call__(
self,
prompt: Union[str, List[str]] = None,
height: Optional[int] = None,
width: Optional[int] = None,
num_inference_steps: int = 50,
guidance_scale: float = 7.5,
negative_prompt: Optional[Union[str, List[str]]] = None,
num_images_per_prompt: Optional[int] = 1,
eta: float = 0.0,
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
latents: Optional[torch.FloatTensor] = None,
prompt_embeds: Optional[torch.FloatTensor] = None,
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
output_type: Optional[str] = "pil",
return_dict: bool = True,
callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,
callback_steps: int = 1,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
guidance_rescale: float = 0.0,
):
几个比较关键的输入:prompt是提示词,height和width即图像长宽(最好设为512),num_inference_steps即生成的步数,越大效果越好但是你需要等待更久,一般设50,guidance_scale即CFG一半设置为7.5,剩下的暂且不看。
step 0 ~2 检查输入,设置参数
step 3 编码提示词prompt
调用encode_prompt函数,于是我们command/ctrl+左键进入这个函数。它最重要的输入就是prompt这个变量:
def encode_prompt(
self,
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt=None,
prompt_embeds: Optional[torch.FloatTensor] = None,
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
lora_scale: Optional[float] = None,
):
prompt既可以是一句话,也可以是一个由n个prompt组成的列表,还可以是已经编码好的向量。
首先使用tokenizer对prompt进行编码,max_length一般为77(CLIP)。如果你的prompt过长(超过77tokens),在后面的if判断句中会提示你的prompt被截断为77tokens。一般而言,每一个prompt词对应一个token;如果是不存在于token字典中的词,tokenizer会将其分解成两个或以上的tokens:
text_inputs = self.tokenizer(
prompt,
padding=“max_length”,
max_length=self.tokenizer.model_max_length,
truncation=True,
return_tensors=“pt”,
)
这里要注意,tokenizer会自动将你的输入tokens填充到77个tokens,并添加开始token(49406)与结束token(49407)。你可以使用text_inputs.input_ids查看这77个tokens。
之后:
prompt_embeds = self.text_encoder(
text_input_ids.to(device),
attention_mask=attention_mask,
)
prompt_embeds = prompt_embeds[0] #取出’last_hidden_state’
使用text_encoder对tokens进行编码,生成[1,77,768]的编码向量。这个text_encoder是CLIP中的Transformer,所以最后的输出结果就是Transformer中的last_hidden_state。1即batchsize,看你输入了多少prompt。
至此,用于控制生成的文本编码已获取。对于CLIP的Transformer,我们在之后的章节会进入CLIPTextModel类详谈。
代码中还出现了一个negative_prompt,我目前还没怎么读过negative_prompt的论文,因此不太了解其原理;但是如果你没有输入negative_prompt,代码会采用classifier-free guidance(无分类引导器)中的方法,设置为unconditional,即空prompt “”,用于unconditional generation。
最后return了文本编码和空prompt文本编码。
step4 设置时间步数
字面意思
step5 准备随机噪声
我们知道扩散模型的生成环节是对一个标准正态分布的随机噪声进行逐步去噪,因此这一步进入prepare_latents函数准备噪声。由于是SD,这个噪声变为降维后的特征域。
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)
这个函数调用了torch.randn_tensor生成指定大小随机tensor。请牢牢记住这个tensor的大小:[bs,4,64,64],bs是batchsize
如果你一直生成512x512(这个尺寸上基本上不用动),这个tensor的大小就是固定的。如果你想进一步探究unet内部的去噪流程,请牢记这四个数。
此外如果想做可复现研究,请提前定义固定种子的generator,输入到这个函数里面。
step6 Prepare extra step kwargs
ip adapter等
step7 最重要的扩散环节
生成潜变量
将潜变量,时间步,提示词嵌入,复杂插件茶树等输入UNet预测噪声,得到预测噪声
对预测噪声,执行条件引导,论文xxx说这一步能大幅提升生成质量
利用采样器去噪,数学东西很多,具体封装到对应的采样器里面了
最后 解码到图像(这一步是我额外加的)
详细:
for i, t in enumerate(timesteps):
latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
首先,根据classifier-free guidance,设置有条件生成与无条件生成的两个latents。
随后,
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
cross_attention_kwargs=cross_attention_kwargs,
return_dict=False,
)[0]
使用unet进行逐步去噪。UNet我会在后面的章节进入UNet2DConditionModel类进行详细分析。
这里的t请在调试过程中鼠标悬浮于timesteps这个变量,可以清楚的看到DDIM采样的跳步过程(50/1000步)。你可以把DDIMScheduler换为别的如DPMScheduler再看看,我还没试过
这一步使用根正苗红的classifier-free guidance,guidance_scale即CFG,一般取7.5。noise_pred_text即有条件生成,noise_pred_uncond即无条件生成。**论文指出使用classifier-free guidance可以大幅提高生成质量**,具体请探究论文 Classifier-Free Diffusion Guidance:
https://arxiv.org/abs/2207.12598 arxiv.org/abs/2207.12598
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
还没完,得到的noise_pred仅仅是预测的噪声,根据
根据算法第四行,我们利用预测的噪声和当前latent预测一步去噪后的latent。这些讨厌且繁琐的数学都被封装在scheduler里面,只需一步step,非常好用。
latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0]
step8 解码到图像(这一步是我额外加的)
step7之后跳出循环,将latent使用VAE转为图像。对于VAE,我们在之后的章节会进入AutoencoderKL类简单谈谈。
从[bs, 4, 64, 64] -> [bs, 3, 512, 512],成为三通道图像的尺寸,并使用一些处理方法变为PIL格式的图像。这里有一个很讨厌的nsfw检测,我们直接把它删掉最好了。
if not output_type == "latent":
image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False)[0]
image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype)
else:
image = latents
has_nsfw_concept = None
__call__函数返回bs张图像,这样文本生成图像就完成了!
return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept)
取出StableDiffusionPipelineOutput.images就是我们要的bs张图像。
最后我们回到典中典五行代码的最后一行,取出bs张图像的第一张,save到output.jpg里面即可。
pipeline("An image of a beautiful girl").images[0].save('output.jpg')
设计思想学习,这个管道很像一个组装厂,生产线的概念,工厂输入原材料,然后各种机器处理加工组装原材料
管道,输入提示词,提示图等数据,然后管道里面各种组件,对数据加工处理,什么VAE,CLIP,UNet,Controlnet等。
比如前端设计得P图架构,是不是可以这样呢?不用图层架构,只是每个组件处理图片等,得到结果。
UNet
总体Unet结构分析
不同于UNet的持续做卷积, 它用下面的结构把hw转成矩阵结构, 拿去做MHA(multi-head attention):
所以UNet2DCondition就是把UNet降采样后的CNN结构部分替换成了Transformer结构.
总的来说: 卷积占总运算量的49%, 矩阵计算占31%(加上Einsum 总共45%)
VAE Encoder+Decoder
这两个模型是成对使用的所以放在一起. 这两个也是比较妙都是CV+transformer的结构.
其中这两个运算量是最大的因为最大分辨率512x512的处理都在这里. Encoder: 566G MACs Decoder: 1271G MACs. Conv分别占了95%和97%的运算量. Transformer的MatMul基本可以忽略不记.
代码实现
https://zhuanlan.zhihu.com/p/672677777
__init__函数
我们看到__init__函数有超大一堆参数,好消息是大部分已经设好了默认值,我们一个都不用管。
在pipeline的初始化中,StableDiffusionPipeline.from_pretrained函数会自动调用unet文件夹中的config.json,填好这些参数。我们来看看哪些是比较重要的:
#(这里我只选了一些重要的参数)
def init(
self,
sample_size: Optional[int] = 64,
in_channels: int = 4,
out_channels: int = 4,
down_block_types: Tuple[str] = (
“CrossAttnDownBlock2D”,
“CrossAttnDownBlock2D”,
“CrossAttnDownBlock2D”,
“DownBlock2D”,
),
mid_block_type: Optional[str] = “UNetMidBlock2DCrossAttn”,
up_block_types: Tuple[str] = (
“UpBlock2D”,
“CrossAttnUpBlock2D”,
“CrossAttnUpBlock2D”,
“CrossAttnUpBlock2D”),
only_cross_attention: Union[bool, Tuple[bool]] = False,
block_out_channels: Tuple[int] = (320, 640, 1280, 1280),
layers_per_block: Union[int, Tuple[int]] = 2,
attention_head_dim: Union[int, Tuple[int]] = 8,
cross_attention_dim: Union[int, Tuple[int]] = 768, #注意这里在原代码中是1280但config.json中是768
)
(1)基本参数 sample_size, in_channels, out_channels:
(2)down/mid/up_block_types直接定义了U-Net网络的不同的层的结构,这里的CrossAttnDownBlock2D CrossAttnDownBlock2D等,下采样,上采样过程分开
(3)only_cross_attention一定是False,文本控制必然发生cross-attention
(4)block_out_channels,这是CrossAttnDownBlock2D中使用Conv对通道数进行变化。
(5)layers_per_block: CrossAttnDownBlock2D中包含的(ResnetBlock+Transformer2DModel)对数。这就更复杂了,我们在之后的章节会进入CrossAttnDownBlock2D类详谈。
(6)attention_head_dim和cross_attention_dim:我们所说的Transformer是一种多头注意力机制,常见的头数就是8。cross_attention_dim代表文本编码的维数。这两项也都不用动
函数体主要就是,检查参数,创建需要的层,节点,组件。
推理-前向传播
forward(call)
def forward(
self,
sample: torch.FloatTensor,
timestep: Union[torch.Tensor, float, int],
encoder_hidden_states: torch.Tensor,
class_labels: Optional[torch.Tensor] = None,
timestep_cond: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None,
down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None,
mid_block_additional_residual: Optional[torch.Tensor] = None,
encoder_attention_mask: Optional[torch.Tensor] = None,
return_dict: bool = True,
) -> Union[UNet2DConditionOutput, Tuple]:
这个参数没有__init__那么多,但也是只有前三个是常用的:
sample就是latent,timestep是你的扩散模型进行到第几步了;encoder_hidden_states是控制向量也就是文本编码。返回的是latent对应预测的噪声
函数主体也是分成了若干步,和上一篇文章一样我也是一步步对应代码来:
step0 center input if necessary 感觉没什么用
step1 时间编码
是很重要的一部分,我们知道扩散模型是一步步进行的,时间戳step代表了我们进行到了第几步。又由于Transformer需要用到time-embedding,因此我们把当前时间step使用self.time_proj和self.time_embedding编码成Transformer能接受的time-embedding,这样U-Net能正确预测出当前step的噪声。
这个time-embedding被嵌入到所有CrossAttnUp/Mid/DownBlock2D中,十分重要。
step2 pre-process
[bs, 4, 64, 64] -> [bs, 320, 64, 64]
step3 down下采样
将latent按照self.down_blocks的顺序依次forward一遍,注意U-Net一直是一个residual的过程,会保存res_samples用来加到上采样的过程中。
for downsample_block in self.down_blocks:
if hasattr(downsample_block, “has_cross_attention”) and downsample_block.has_cross_attention:
# For t2i-adapter CrossAttnDownBlock2D
additional_residuals = {}
if is_adapter and len(down_block_additional_residuals) > 0:
additional_residuals[“additional_residuals”] = down_block_additional_residuals.pop(0)
sample, res_samples = downsample_block(
hidden_states=sample,
temb=emb,
encoder_hidden_states=encoder_hidden_states,
attention_mask=attention_mask,
cross_attention_kwargs=cross_attention_kwargs,
encoder_attention_mask=encoder_attention_mask,
**additional_residuals,
)
else:
sample, res_samples = downsample_block(hidden_states=sample, temb=emb, scale=lora_scale)
if is_adapter and len(down_block_additional_residuals) > 0:
sample += down_block_additional_residuals.pop(0)
down_block_res_samples += res_samples
这里用了一个很巧妙的方法判断是哪个down_blocks:
如果是CrossAttnDownBlock2D,那么它会出现一个属性叫做has_cross_attention,就会走第一个if分支;
如果是DownBlock2D,就会走第二个分支。
仅第一个分支输入了encoder_hidden_states,也就是cross-attention要用到的文本编码。
这个版本添加了对ControlNet的支持,也就是downsample_block旁边多了一些新的block,那么对这些新block也forward一下就好了。ControlNet也添加了midblock和upblock的旁置,后文略
直接循环执行所有block层,包括cnn,形状变换,attention,就是说torch可以直接完成这些转换动作。
step4 mid
将latent输入到mid_block进行一次forward,具体它存在的意义我应该去看看U-Net这篇文章。
step5 up上采样
和down部分很像,不过由于添加了res_sample部分,
res_samples = down_block_res_samples[-len(upsample_block.resnets) :]
down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)]
直接提取出来就好了。upsample_block中会输入这些res_sample,做一个求和。
step6 post-process
[bs, 320, 64, 64] -> [bs, 4, 64, 64]
最后使用一个UNet2DConditionOutput,以tensor的形式返回预测的噪声。其实我不知道为什么非要另设一个Output类来返回结果,可能好看一点?》
以上是StableDiffusionPipeline的基本架构,可以看到里面仍然有大量黑盒子比如CrossAttnDownBlock2D,CrossAttnMidBlock2D,DownBlock2D等等,我在上面只是介绍了latent在forward()函数里面是怎么流动的,并没有介绍latent发生了什么变化,以及有什么意义。
这是因为每一个黑盒子仍然包含了大量小黑盒子,每个小黑盒子里面又包含了许多小小黑盒子,往往复复这篇文章的字数就要爆炸了,也不方便我们进一步理解。因此我也暂时采用这种自顶向下的讲解方法,先讲大模块,再讲每个大模块对应的小模块。下一章我们会对U-Net内部大小盒子进行详细讲解,这里我先放上一张图作为剧透:
至此你已经学会了StableDiffusionPipeline的最基本使用方法和整体架构,如果想要学习更底层的原理则必须去了解这些大小盒子,因此强烈建议大家阅读我的下一篇文章,也是StableDiffusionPipeline的最后一节。
UNet内部的详细结构
首先我直接放图,里面是Stable Diffusion U-Net里的所有模块(也就是上一章说到的大小盒子)。
图中所有英文词语均直接取自Diffusers==0.24.0代码中,我认为官方短期之内是不会随便改名的。
我们来解析一下图中各个词语。
(1)我们设UNet2DConditionModel为一级结构,其内部包含input_blocks,middle_blocks和output_blocks,它们各自为一个 nn.ModuleList([])(也就是神经网络中专用的list,可以直接forward());
(2)每个一级结构的nn.ModuleList包含若干二级结构,例如CrossAttnDownBlock2D3,DownBlock2D1等;当一级结构的nn.ModuleList执行forward函数后,则顺序执行每个二级结构的forward函数;
(3)每个二级结构又包含了若干三级结构,例如ResnetBlock2D,Transformer2DModel,DownSample2D(这里要和DownBlock2D区分开);
(4)每个三级结构又包含了若干四级结构,但是图片边幅限制画不出来了。例如ResnetBlock2D就包含了若干熟悉的残差块;Transformer2DModel在过去的diffusers版本也叫做SpatialTransformer(也就是Visual Transformer),主要包含了12个BasicTransformerBlock;这里有兴趣的朋友可以去看VIT相关文章
(5)事实事实上,BasicTransformerBlock就是我们最常见的Transformer块,但是我不打算把它设为五级结构了,基本上就是self-attention,feed-forward,cross-attention与LayerNorm四步,我会在Diffusers代码级讲解(三/四/五)中讲解注意力操控时重点介绍这一部分。
为了方便理解,我把一个三级结构的forward简写为一个字母,则二级结构CrossAttnDownBlock2D的forward函数简写为RTRTD,即按顺序的两次resnet forward,两次Transformer2DModel forward和一次降采样。
以上是为了方便大家迅速掌握U-Net的组成与forward流程,如果想学会如何快速掌握代码框架,强烈大家设好断点自己调试一遍,画出文首结构图。这个过程也许十分枯燥,但绝对会使你的代码能力有质的飞跃。
作者这个总结非常好,通过多级结构来理解网络 更多的,我们理解代码结构的时候也可以灵活些,通过图示啊,多级结构阿等方式理解,不要那么固定死板
上一篇文章我们讲了UNet2DConditionModel这个一级结构与代码,那么本文讲解二级结构,三级结构比较简单,就是很简单的四级结构连接;四级结构其实已经算很小的模块;于是三四级我就不仔细讲了。
这下CrossAttnDownBlock2D就清晰了,由 ResnetBlock2D,Transformer2DModel,ResnetBlock2D,Transformer2DModel,DownSample2D组成,简称RTRTD,三个CrossAttnDownBlock2D发生三次下采样;
第四个DownBlock2D包含两个三级结构ResnetBlock2D和一个可选的三级结构DownSample2D,但由于is_final_block=True,因此不会add_downsample,这个DownSample2D就没有了。
CrossAttnDownBlock2D的forward就是轮流forward一遍 RTRTD;三个CrossAttnDownBlock2D一起forward就是 RTRTD,RTRTD,RTRTD;再经过一个DownBlock2D,也就是RR
到这里一级结构中的input_blocks就结束了。
UNet2DConditionModel的Forward
非常简单,就是上面这些东西按顺序forward,forward一遍 RTRTD,RTRTD,RTRTD,RR,见下面
下面内容非常非常重要,如果你不想搞懂上面复杂的东西请一定要看看下面的
我们看一下latent尺度变化:
输入bs x 320 x 64 x 64 -> RTRTD -> bs x 320 x 32 x 32 -> RTRTD -> bs x 640 x 16 x 16 -> RTRTD -> bs x 1280 x 8 x 8 -> RR -> bs x 1280 x 8 x 8
因此在U-Net下采样之后,最后我们获得的就是 bs x 1280 x 8 x 8 的latent。bs是batchsize
这篇文章对于想要掌握注意力操控、ControlNet,LoRA等模块的嵌入等十分重要,你必须时刻对latent的变化心中有数才能在敲代码时游刃有余,而不是不知道为什么突然就报错:
RuntimeError: The size of tensor a (2048) must match the size of tensor b (2088) at non-singleton dimension 1
当然,整个调试过程真的非常非常枯燥,也非常非常锻炼人,如果你想真正提高diffusers代码能力,接下来是一个很有挑战性的作业: