LLM大模型: 生成式模型generative model SD和VAE的数学原理和prompt融入image
1、(1)上文介绍了DDPM生成图片的原理和代码测试结果,训练时给样本图片加上gaussian noise,预测时也是预测gaussian noise;
- 这里为啥要用gaussian distribution?为啥不用其他的分布?
- 高斯分布相对比较简单,只有两个参数:均值和方差,容易控制;
- 为啥一张随机生成的gaussion noise经过很多次裁剪后能得到想要的图片?数学上的依据是什么?
- 理论上讲: 任意K个高斯分布按照特定的权重组合,能得到任意曲线,也就是拟合任意的分布
-
- 假设有K个高斯分布,这K个高斯分布称作混合模型的隐变量则复杂分布的概率分布是:
通过这种Gaussian分布拟合任意分布,这下知道为啥diffusion模型会使用上千个Gaussian noise来生成 image了吧?本质就是利用Gaussian分布的组合生成所需image!两个乘数一个是DDPM中的alpha,另一个是epsilon(预测noise的网络).一张样本图片,比如是3 channel * 28 width * 28 height = 2352个像素点。理论上讲,只要有一个像素点不同,图片就不同。再说直白一点:每个像素点的值刚开始都是随机生成的,生成的值符合Gaussian~(0,1)分布;后续迭代很多次(因为是Gaussian~(0,1)分布,每次生成的数值都较小,99.7%的数值会在(-3,3)之间。为了满足channel的数值范围(一般是0~255),需要多次迭代。比如channel的数值是240,随机生成的noise值是3,那么至少迭代80次才能满足要求),每次迭代都会用新生成的值(也符合Gaussian分布)加减初始值,直到迭代结束(这个思路和"小步快跑"的梯度下降没任何区别)。2352个像素点拼接起来就生成了预测的noise图片!
(2)怎么求seta?以VAE为例,整个网络结构如下:
Z是隐变量,经过seta网络生成X;如果这个生成的X和原来数据集一样,说明seta是正确的。那么既然x已经发生了,最合理的思路就是让X的概率最大化了,也就是max likelyhood!也就是seta网络要让X生成的概率最大,以此来得到最合适的seta网络参数!
2、实战时,肯定是要加入用户输入的prompt的!怎么严格按照用户的prompt生成image了?transformer架构最初是用来做翻译的,encoder把一种语言的输入转成embedding后,通过cross attention的机制把输入信息转移到decoder用于生成输出的token,这里也需要把输入的prompt信息传递到图片的decoder部分,是不是也能借鉴一下这个思路了?
用户输入的prompt经过矩阵的线性转换后生成了embedding,在每个resnetblock后都加上一个transformer block(down 和 up 都要加),通过这种方式把用户的prompt信息融入整个unet网络!
具体代码实现,参考如下:像素点是query,prompt是key和value,做cross attention!注意:为啥要用像素点做attention?生成最终的image,需要每个像素点都参与,只有每个像素点的值对了,最终的image才能正确!为了确保每个像素点都正确,需要把prompt的值融入!
import torch from torch import nn from config import * import math class CrossAttention(nn.Module): def __init__(self,channel,qsize,vsize,fsize,cls_emb_size): super().__init__() self.w_q=nn.Linear(channel,qsize) self.w_k=nn.Linear(cls_emb_size,qsize) self.w_v=nn.Linear(cls_emb_size,vsize) self.softmax=nn.Softmax(dim=-1) self.z_linear=nn.Linear(vsize,channel) self.norm1=nn.LayerNorm(channel) # feed-forward结构 self.feedforward=nn.Sequential( nn.Linear(channel,fsize), nn.ReLU(), nn.Linear(fsize,channel) ) self.norm2=nn.LayerNorm(channel) def forward(self,x,cls_emb): # x:(batch_size,channel,width,height), cls_emb:(batch_size,cls_emb_size) x=x.permute(0,2,3,1) # x:(batch_size,width,height,channel) # 像素是Query Q=self.w_q(x) # Q: (batch_size,width,height,qsize) Q=Q.view(Q.size(0),Q.size(1)*Q.size(2),Q.size(3)) # Q: (batch_size,width*height,qsize) 每个像素点都要参与attention的计算 # prompt是Key和Value K=self.w_k(cls_emb) # K: (batch_size,qsize) K=K.view(K.size(0),K.size(1),1) # K: (batch_size,qsize,1) V=self.w_v(cls_emb) # V: (batch_size,vsize) V=V.view(V.size(0),1,V.size(1)) # v: (batch_size,1,vsize) # attention打分矩阵Q*K attn=torch.matmul(Q,K)/math.sqrt(Q.size(2)) # attn: (batch_size,width*height,1) attn=self.softmax(attn) # attn: (batch_size,width*height,1) # print(attn) # 就一个Key&value,所以Query对其注意力打分总是1分满分 # attention输出 Z=torch.matmul(attn,V) # Z: (batch_size,width*height,vsize) Z=self.z_linear(Z) # Z: (batch_size,width*height,channel) Z=Z.view(x.size(0),x.size(1),x.size(2),x.size(3)) # Z: (batch_size,width,height,channel) # 残差&layerNorm Z=self.norm1(Z+x)# Z: (batch_size,width,height,channel) # FeedForward out=self.feedforward(Z)# Z: (batch_size,width,height,channel) # 残差&layerNorm out=self.norm2(out+Z) return out.permute(0,3,1,2) if __name__=='__main__': batch_size=2 channel=1 qsize=256 cls_emb_size=32 cross_atn=CrossAttention(channel=1,qsize=256,vsize=128,fsize=512,cls_emb_size=32) x=torch.randn((batch_size,channel,IMG_SIZE,IMG_SIZE)) cls_emb=torch.randn((batch_size,cls_emb_size)) # cls_emb_size=32 Z=cross_atn(x,cls_emb) print(Z.size()) # Z: (2,1,48,48)
把attention机制打包到convblock中:
from torch import nn from cross_attn import CrossAttention class ConvBlock(nn.Module): def __init__(self,in_channel,out_channel,time_emb_size,qsize,vsize,fsize,cls_emb_size): super().__init__() self.seq1 = nn.Sequential( nn.Conv2d(in_channel,out_channel,kernel_size=3,stride=1,padding=1), # 改通道数,不改大小 nn.BatchNorm2d(out_channel), nn.ReLU(), ) self.time_emb_linear=nn.Linear(time_emb_size,out_channel) # Time时刻emb转成channel宽,加到每个像素点上 self.relu=nn.ReLU() self.seq2=nn.Sequential( nn.Conv2d(out_channel,out_channel,kernel_size=3,stride=1,padding=1), # 不改通道数,不改大小 nn.BatchNorm2d(out_channel), nn.ReLU(), ) # 像素做Query,计算对token的attention,实现分类信息融入图像,不改变图像形状和通道数 self.crossattn=CrossAttention(channel=out_channel,qsize=qsize,vsize=vsize,fsize=fsize,cls_emb_size=cls_emb_size) def forward(self,x,t_emb,cls_emb): # t_emb: (batch_size,time_emb_size) x=self.seq1(x) # 改通道数,不改大小 t_emb=self.relu(self.time_emb_linear(t_emb)).view(x.size(0),x.size(1),1,1) # t_emb: (batch_size,out_channel,1,1) output=self.seq2(x+t_emb) # 不改通道数,不改大小 return self.crossattn(output,cls_emb) # 图像和prompt embedding做attention
3、模型微调:市面上可能有已经训练好的模型,但模型的训练数据大概率是通用的数据,并不是某些垂直细分领域的数据,怎么才能加上自己所需垂直领域的数据了?最合适的当然是微调了!微调的方式也有很多:全量参数微调、冻结部分参数微调、lora微调。如果训练数据有限、算力也有限,那么最合适的就是lora微调了!理论上讲,任何线性变换(直白一点就是矩阵乘法啦)都可以旁挂两个m*r和r*n的小矩阵来完成lora微调!但是:这种任务的核心是根据prompt生成image(所以微调的样本肯定也有配对的prompt和image),重点就是融合prompt和image的cross attention了,所以这里直接在cross attention的矩阵乘法旁边外挂新矩阵来达到融合新样本信息的目的!
先找到需要旁挂小矩阵的层:
from unet import UNet from dataset import train_dataset from diffusion import forward_diffusion from config import * import torch from torch import nn from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter import os from lora import inject_lora EPOCH=200 BATCH_SIZE=400 if __name__=='__main__': # 加载模型 model=torch.load('model.pt') # 向nn.Linear层注入Lora for name,layer in model.named_modules(): name_cols=name.split('.') # 找到cross attention中的线性变换,也就是矩阵乘法 filter_names=['w_q','w_k','w_v'] if any(n in name_cols for n in filter_names) and isinstance(layer,nn.Linear): inject_lora(model,name,layer) # lora权重的加载 try: restore_lora_state=torch.load('lora.pt') model.load_state_dict(restore_lora_state,strict=False) except: pass model=model.to(DEVICE) # 冻结非Lora参数 for name,param in model.named_parameters(): if name.split('.')[-1] not in ['lora_a','lora_b']: # 非lora部分不计算梯度 param.requires_grad=False else: param.requires_grad=True dataloader=DataLoader(train_dataset,batch_size=BATCH_SIZE,num_workers=4,persistent_workers=True,shuffle=True) # 数据加载器 optimizer=torch.optim.Adam(filter(lambda x: x.requires_grad==True,model.parameters()),lr=0.001) # 优化器只更新Lorac参数 loss_fn=nn.L1Loss() # 损失函数(绝对值误差均值) print(model) writer = SummaryWriter() model.train() n_iter=0 for epoch in range(EPOCH): last_loss=0 for batch_x,batch_cls in dataloader: # 图像的像素范围转换到[-1,1],和高斯分布对应 batch_x=batch_x.to(DEVICE)*2-1 # 引导分类ID batch_cls=batch_cls.to(DEVICE) # 为每张图片生成随机t时刻 batch_t=torch.randint(0,T,(batch_x.size(0),)).to(DEVICE) # 生成t时刻的加噪图片和对应噪音 batch_x_t,batch_noise_t=forward_diffusion(batch_x,batch_t) # 模型预测t时刻的噪音 batch_predict_t=model(batch_x_t,batch_t,batch_cls) # 求损失 loss=loss_fn(batch_predict_t,batch_noise_t) # 优化参数 optimizer.zero_grad() loss.backward() optimizer.step() last_loss=loss.item() writer.add_scalar('Loss/train', last_loss, n_iter) n_iter+=1 print('epoch:{} loss={}'.format(epoch,last_loss)) # 保存训练好的Lora权重 lora_state={} for name,param in model.named_parameters(): name_cols=name.split('.') filter_names=['lora_a','lora_b'] if any(n==name_cols[-1] for n in filter_names): lora_state[name]=param torch.save(lora_state,'lora.pt.tmp') os.replace('lora.pt.tmp','lora.pt')
旁挂两个小矩阵的实现:
from config import * import torch from torch import nn import math # Lora实现,封装linear,替换到父module里 class LoraLayer(nn.Module): def __init__(self,raw_linear,in_features,out_features,r,alpha): super().__init__() self.r=r self.alpha=alpha self.lora_a=nn.Parameter(torch.empty((in_features,r))) self.lora_b=nn.Parameter(torch.zeros((r,out_features))) nn.init.kaiming_uniform_(self.lora_a,a=math.sqrt(5)) self.raw_linear=raw_linear def forward(self,x): # x:(batch_size,in_features) raw_output=self.raw_linear(x) lora_output=x@((self.lora_a@self.lora_b)*self.alpha/self.r) # matmul(x,matmul(lora_a,lora_b)*alpha/r) return raw_output+lora_output def inject_lora(model,name,layer): name_cols=name.split('.') # 逐层下探到linear归属的module children=name_cols[:-1] cur_layer=model for child in children: cur_layer=getattr(cur_layer,child) #print(layer==getattr(cur_layer,name_cols[-1])) lora_layer=LoraLayer(layer,layer.in_features,layer.out_features,LORA_R,LORA_ALPHA) setattr(cur_layer,name_cols[-1],lora_layer)
4、普通的AE:encoder把原始image转成latent,为了直观理解latent,可以把latent看成是image的特征,比如image是人,那么latent每个维度可能是笑容、性别、头发、眼睛、嘴巴等!单存只看latent的值,没有任何潜在的语义信息,比如猫、狗、土豆、大白菜之间的latent完全没有区分度(理想情况是猫、狗的latent接近,土豆和大白菜的latent也接近);这里仅仅是信息压缩,decoder只能还原train数据集中image的latent。如果给decoder输入的latent不是train集中image的latent,decoder是无法生成高质量image的!这里只能得到相同的reconstruct!【这种情况就是单存的compress或encode,可以当成是加密算法来用】
举例说明:原始图像是7,对应latent是Zref;如果直接把Zref输入decoder,是能reconstruct得到7的;但如果在Zref附近随机抽样,通过decoder后得到的图片质量就很差了!所以这种情况就不能叫generative了(不知道latent的取值分布,无法自由sample latent的值,此时不能说过拟合/泛化性差,只能说没有任何泛化能力),只能叫压缩和解压!
一旦train结束,latent就固定了!此时直接用decoder解码,生成的图片会接近train数据集的图片,这样做毫无意义:如果输入和输出的image一样,直接copy不就行了?还训练啥encoder和decoder了?image是由latent经过decoder生成的,因为decoder在训练完后也固定了,为了生成不同的image,只能让latent不一样,这又该怎么做了? 这就只能随机在N(0,1)之间生成noise图片了,然后和latent运算后再进度decoder!这样一来,原来AE中固定值的latent变成了一个不确定的概率分布,才能进一步通过decoder生成与tain集中不同的image!换个角度理解:encoder把训练image所有的图片都映射到N(0,1),然后latent就在这个范围内sample取值,decoder也能正常生成新image,这就是VAE的思路!下面是两个不同的sample后得到的latent
VAE:和sd相比,VAE训练的时候,核心是sample一个noise,然后和latent运算后再经过decoder生成image,这里只sample一个noise,而不是sd的1000个noise!
训练好后就要生成新image了,此时直接在N(0,1)范围内sample noise图片后进度decoder即可!因为train的时候已经加了噪声,所以Z的取值比较多;生成新image的时候随机产生的noise可能就在训练时Z的附近,甚至是重合一样的z,所以生成图片的质量明显高很多!
为了直观理解生成image时随机sample的noise的作用,可以把noise想象成图片的描述,比如noise每个维度都描述了猫的特征:尖耳、白肚、斑马纹路黑背、扭头看后(让人画画不也是一样的思路么:先确定要画的内容特征,然后再执笔完成)......
sampling时随机生成的latent,如果用于multimodal,比如text生成image,可以用bert提取nlp的presentation,然后和P(z)融合(比如直接相加),再用融合后的P(z')通过decoder生成image!
生成图片是中间noise可以根据需求减去或加上需要的向量,本质就是改变图片的描述,然后通过decoder生成image!比如这里的臭脸和笑脸,可以从其他image利用encoder提取,也可以使用bert从nlp提取!
噪声epsilon和均值、方差之间的计算方法如下,最后得到的z就是sampled latent attributes!
VAE的loss有两部分:
- input和output图片的之间的差异,用MSE表示;目的是约束output尽可能接近input,不能天马行空,比如input是猫,ouput变成狗了;效果直观如下:
- 还有就是KL散度,让latent的分布接近N(0,1);初衷是为了防止过拟合(latent在image附近也能很好地reconstruct,而不局限于刚好就是image的latent),实践时可以让latent在某个概率下生成,不再是固定值,达到了生成新image的效果!本质是训练时让decoder尽量多地处理不同数值的latent(换句话说,latent空间处理的数值越多,decoder的“见识”越广,“经验”越丰富,这就类似平时做题应对期末考试一样),后续生成image的时候如果遇到了和训练时的latent接近甚至一样,生成的image质量自然就高了!
加了noise的latent能平滑地从一个image生成到另一个image!VAE生成的latent是连续的、紧凑和平滑的!
总结:
1、 机器学习核心是根据输入数据得到所需的输出数据,肯定要对输入数据做各种转换,常见的做法就是matrix multi、active、attention等:
- matrix multi:旧的向量转移到新的空间
- 通过更改matrix的参数让新向量的数值适配下游任务
- 向量长度做调整适配
- active:特征多维组合后生成新特征,用于下游任务
- attention:相似度的计算,用于不同网络之间的信息传递与融合
2、VAE换个角度理解: 比如要生成一张256*256的图片,一共有256*256*3=196608个维度要找到合适的值!每个维度按照255个取值计算,一共有255^196608种可能,这是个天文数字!如果随机生成一组数字,得到我们想要图像的概率接近0,所以直接在原始概率空间分布sample是不可能的!怎么办了?既然原始概率空间太大,直接sampling和generate不现实,怎么办?
既然原始空间太大无法sampling,那换个小空间sampling行不?这就是encoder的思路:把image压缩到很小的latent空间(比如只有32维),相当于提取image的特征了(比如耳朵、鼻子、眼睛、嘴巴、微笑等特征),然后通过decoder还原成高质量的image!生成image的时候从latent空间随机sampling,相当于得到image的特征,然后通过decoder还原!
后续的sd生成图片,也需要在latent空间sampling图片(维度少,并且取值范围在N(0,1)之间也可控,更利于后续denoise时候的"雕刻"),然后通过用户的prompt修改latent,得到合适的image特征,再通过decoder生成image!
参考:
1、https://www.bilibili.com/video/BV19H4y1G73r/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2
2、https://nn.labml.ai/diffusion/stable_diffusion/model/unet.html
3、https://deepsense.ai/diffusion-models-in-practice-part-1-the-tools-of-the-trade/
4、https://aitechtogether.com/python/77485.html
6、https://www.bilibili.com/video/BV1op421S7Ep/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 VAE公式推导
7、https://www.bilibili.com/video/BV1D54reZE2g/?spm_id_from=333.788.recommend_more_video.0&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 VAE可视化解释