【技术杂谈】聊聊SD那档子事:clip、vae、unet、controlnet 和 lora

【技术杂谈】聊聊SD那档子事:clip、vae、unet、controlnet 和 lora

起笔于2024/2/23 归档一下自己的抽象理解

如果后续有了些别的理解就继续顺着更新

先说说sd吧,主要思路是:当我们想要生成一个玩意的时候,比如一幅画——现实的画不是一次喷涂就完成了的,从一张白纸到一幅画中间经历的很多过程,我觉得我们不太可能真的去记录这个过程,然后去搞个模型去学 一笔一划 这个过程,不太可能

那就先办法简化被,白纸→画作 === 纯粹的噪声→ 有意义的图像 ——这个如何,每个步骤都可以理解为 我们一步一步往正常图像上添加噪声的 反向行为——也就是 降噪

如果可行的话,现在我们需要一个架构来支持我们完成这个功能的学习

这个世界是一个巨大的 编码器-解码器

transformer 是encoder-decoder,vae 也是encoder-decoder, clip在里头也当作一个text-encoder,unet说实话感觉也像是个encoder-decoder,controlnet更是把unet的上一半当作了image-encoder——我去,这个世界是一个巨大的编码器-解码器

那或许这个架构就够用了

从ae到vae

我们可以从ae开始讲,前人们可能早就试过了直接把图像本身进行变换或者生成,但是效果自然不佳,重点在于如何把信息有效的那部分提取出来,这就需要数据的降维——此时的数据是人肉眼无法直观理解的东西——但是往往一个任务最后还是需要回到我们肉眼可见之处,所以我们需要把数据变回来,所以我们还需要一个解码器,但是都人工智能了,能不能不要我们自己去想编解码的算法,它自动编码呢——ae(自动编码器)就是这么来的

ae 期望一个A进来,经过encoder得到一个z ,经过decoder又回到了A,其实,这个想法很好,而且但看后半部分,如果我们可以直接提供一个z,然后直接经过decoder ,这不就相当生成了一个图像吗!

但是实际上ae很难做到这点,如果你训练的数据是一些手写数字,然后由给了一个苹果的z来decoder,那很可能只会得到一个噪声图像——我们对于z赋予了过多的期望,但是忘记了没有施加限制的z的可能性太多了,ae其实其实在采样到了它之前训练样本的那些,相似或许还能work,但是离太远的终究以这个网络结构难以学到——是,我们或许可以继续增加模型,增加样布,但这似乎就不太划算了——其实也正常,毕竟我们并不知道z应该是符合什么规则的,我们只是期望神经网络可以学到z是什么样子的,a-z-a的过程看作一个隐含马尔可夫过程,我们其实只知道a的分布

那如果我们对这个z进行限制呢?我们假设z的分布呢,这似乎就可以用 变分贝叶斯 的方法去论证这个过程的可行性,这就有了ae的加强版:VAE(变分自动编码器)

其实就是预设了z服从某个多元高斯分布,我们会有一个定死了的方差,以此预测一个均值——其实这个均值可以拿来用当作z,或者也可以分布确定了在采样一个出来。其实没啥说的,主要是反向传播的时候,我们其实没有真正的z, 这里vae有个训练的小trick,就是捏一个z’=z+N(0,1) 方差 | 这个*是逐元素相乘 ——这一整个训练就可以跑通了

这样 vae奠定了可以信任的特征空间的基础,因为前面z是隐含的嘛,我们称之 Latent 空间/图像

unet作为大骨架

那现在我们需要一个可以将这个latent进行不断降噪得到新的latent,然后在给vae解码出真实图像

unet就是负责这个过程的,但是即使是已经全是特征的数据不降维地进行运算,就已经是非常大的运算量了,那还是得进一步缩小尺寸——其实就还是编码解码这一套,只不过是往降噪这个方向去学

非常有意思的一点是,我们都知道缩放会丢失信息,,而且网络太深也会丢失信息,前者unet使用在相同尺寸下的下采样和上采样之间增加一个连线,有一部分输出也就是跳过中间层的运算防止丢失全部信息,这也就是为啥是UNET,因为这种跳线使整个网络不得不呈现一个U形状,而且这个结构是递归的,我们先有一个初始的中间层——里头是残差块——然后左右接下上采样和采样(缩放)后连接的残差块——我们再把这个结构再视作一个中间层,继续左右采样;持续好几层

这个结构很神奇,而且中间有残差块(resnet里也是有跳线,输出是两个卷积块和另一个独立的卷积块跳过来的总和),这也避免信息丢失。

总之这个结构足够支撑我们降噪出一个图像了

clip作为文本编码器

但是对于实际的生成我们需要一些引导,比如用prompt来描述画面里应该包含什么玩意,当然这部分信息是需要经过tokenize ,然后可能还有一些text embedding,让给Clip,用里头的文本编码器的部分,去做一系列特征的提取(transformer的环节),最后会得到一个token数量*特征数的一个 condition 传给unet——额外插一句,token数量是有限的,超过就得截断,不超过就补全——然后为了引导在unet的生成过程,需要在其内部加入 cross attention ——其K V就来自 这个text condition ,然后我们latent的特征就作为Q

上面这些玩意其实就是sd的一个基本架构了

Lora作为微调手段

但是我们还是会面对一些更具体的要求,需要微调,lora就是一个不错的方法

其实如果我们对于一个生成的过程看作是 Y=W*x,其实我们要是调整模型实际就是在调整W的权重,但是呢,W太大了,所以我们微调起来很困难,但是我们可以把对于W的调整量分解出来,

比如W是h * h的尺寸,h很大,但是我们可以分解成两个比较低rank的矩阵A 和 B :A 是h * r,B是r * h

微调的时候 W’=W+ 权重*AB,而且因为我们的r相较h是很低的,所以比较容易训练,而且在实践中发现即使是这种线性的微调,也可以获得一个很好的微调效果。

而在sd中,lora的微调也是在线性的层进行的,主要是clip内部有attention层,那就有 Q K V 的权重,Unet为了接受text condition 也要有attention ,也有 Q K V 的权重——这也就是为什么我们会看到 lora的训练选项有 clip 和 unet的选项。

controlnet作为image condition

其实光有prompt还是没法满足一些需求,我觉得必须要承认,图像本身携带了很多难以用标签或者文本描述的东西,所以有一个image condition 也很重要,所以就有了controlnet这个大杀器,

要想要image condition ,那相当于我们需要有一个 image encoder, 其实clip就有image endcoder,但是controlnet没有用它,反而利用了unet的下采样部分——哈哈 这不就是现成的一个encoder——所以controlnet在训练的时候复制了底模权重,然后再各层之间加上初始化为0的1*1卷积,算是个训练的trick吧,不改变输出尺寸同时让训练开始时输出为0来模拟逐渐学习“通过图像控制”的能力,其输出就是各个下采样的输出逐个跳线到对应的上采样的unet的位置——有点像外置了一个管道,没有影响原始的sd架构,然后这个控制效果就因训练而异——我觉得这是很天才的一点,因为图像本身携带的作为一种控制信息的东西其实太多了,同个图像通过不同的处理可以获得不同的信息,无论是为了学习难度还是为了便于接入到生产流程,这个思路都很正确。