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网络!

        0

   具体代码实现,参考如下:像素点是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的取值比较多【encoder是N(0,1)分布,加的noise也是N(0,1)分布,所以latent Z还是N(0,1)分布】;生成新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!

3、DETR:虽说是object detection的,但本质还是Gen AI,所以和VAE至少在框架结构上是一样的!整体网络结构如下:

 

   VAE的encoder和decoder完全可以借鉴transformer架构,细节改改就行了!encoder提取的特征直观感受:明显把框架都勾勒出来了!

  

   细节架构图:

  

   decoder:

  • self attn:object queries内部之间做attn,主要是互相之间协商好,把识别的目标任务分配好!比如query1识别牛,query2识别🐖,query3识别猫.....
  • 首先随机初始化object queries(0+位置编码): object queries直接用0初始化,再加上位置编码;位置编码用来初始化vec;不同位置编码的queries vec关注不同image的位置:先对位置敏感,再对关注内容敏感!如果不加位置编码,可能导致很多object queries都关注同样的object,多样性或泛化性太差

 

 

 

参考:

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

5、https://www.bilibili.com/video/BV1ix4y1x7MR/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 

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可视化解释

posted @ 2024-09-25 17:09  第七子007  阅读(50)  评论(0编辑  收藏  举报