手撕Diffusion系列 - 第十一期 - lora微调 - 基于Stable Diffusion(代码)

手撕Diffusion系列 - 第十一期 - lora微调 - 基于Stable Diffusion(代码)

目录

  • 手撕Diffusion系列 - 第十一期 - lora微调 - 基于Stable Diffusion(代码)
  • Stable Diffusion 原理图
    • Stable Diffusion的原理解释
    • Stable Diffusion 和Diffusion 的Unet对比
  • Lora 微调原理
  • Stable Diffusion 添加lora微调代码
    • Part1 添加lora.py文件 - 用于设置lora层以及替换
      • 1. 引入相关库函数
      • 2. 定义LoraLayer的类
      • 3. lora层的替换
    • Part2 添加lora_finetune.py,用于参数微调训练得到lora参数.pt文件
      • 1. 引入相关库函数
      • 2. 替换模型中的注意力机制里面的Wq, Wk, Wv,替换线性层
    • Part3 修改 denoise.py,修改测试的时候的lora参数加入
      • 1. 引入相关库函数
      • 2. 定义去噪的函数
      • 3. 测试-去噪
  • 参考

Stable Diffusion 原理图

Stable Diffusion的原理解释

Stable Diffusion的网络结构图如下图所示:

在这里插入图片描述

Stable Diffusion 的网络结构图 ## Stable Diffusion 和 Diffusion 的区别
  • 改动1:利用 AE,VAE,VQVAE 等自编码器,进行了图像特征提取,利用正确提取特征后的图像作为自己原本在Diffusion中的图像
  • 改动2:在训练过程中,额外添加了一些引导信息,促使图像生成,往我们所希望的方向去走,这里添加信息的方式主要是利用交叉注意力机制(这里我看图应该是只用交叉注意力就行,但是我看视频博主用的代码以及参照的Stable-Diffusion Unet图上都是利用的Transoformer的编码器,也就是得到注意力值之后还得进行一个feedforward层)。
  • **改动3:**利用 AE,VAE,VQVAE 等自编码器进行解码。(这个实质上和第一点是重复的)
  • **注意:**本次的代码改动先只改动第二个,也就是添加引导信息,对于编码器用于减少计算量,本次改进先不参与(555~,因为视频博主没教),后续可能会进行添加(因为也比较简单)

Stable Diffusion 和Diffusion 的Unet对比

在这里插入图片描述

原本的Unet图像

在这里插入图片描述

Stable Diffusion的 Unet 图像
  • 我们可以发现,两者之间的区别主要在于,在卷积完了之后添加了一个Transformer的模块,也就是其编码器将两个信息进行了融合,其他并没有改变。
  • 所以主要区别在卷积后的那一部分,如下图。

在这里插入图片描述

卷积后的区别
  • 这个ResnetBlock就是之前的卷积模块,作为右边的残差部分,所以这里写成 了ResnetBlock。
  • 因此,如果我们将Tranformer模块融入到Restnet模块里面,并且保持其输入卷积的图像和transformer输出的图像形状一致的话,那么就其他部分完全不需要改变了,只不过里面多添加了一些引导信息(MNIST数据集中是label,但是也可以添加文本等等引导信息) 而已。

Lora 微调原理

  • LoRA算法

在这里插入图片描述

LoRA 微调算法 - 初始示意图
  • 算法过程:对于原先的参数不改变,通过右边添加一个参数矩阵来进行微调,也就是利用新的参数矩阵来微调拟合新领域的参数和初始参数的差距。也就是ΔW。

理论:预训练大型语言模型在适应新任务时具有较低的“内在维度” , 所以当对于一个预训练模型来说,原先的参数是有非常多的冗余的,因此我们可以利用低维空间(也就是降维)去表示目标参数和原先参数之间的距离。因此ΔW是相对W来说维度非常小的,减少了非常多的参数量。

在这里插入图片描述

LoRA参数微调具体表现
  • 因为要保证输入和输出的维度和原本的参数W一样,所以一般参数输入的维度还是相同的,但是中间的维度小很多,从而达到减少参数量的结果。比如原本是100x100的参数量,现在变为100x5(r)x2,减少了10倍。

    • 其中r就是低秩的那个秩数。可以自定义。

    o u t p u t = n e t ( x ) + t o r c h . m a t m u l ( x , t o r c h . m a t m u l ( l o r a a , l o r a b ) ∗ a l p h a ( 可能这里也会除以 r ) output=net(x)+torch.matmul(x,torch.matmul(lora_a,lora_b)*alpha(可能这里也会除以r) output=net(x)+torch.matmul(x,torch.matmul(loraa,lorab)alpha(可能这里也会除以r)

alpha或者alpha/r 是一个缩放因子,用于调整组合结果(原始模型输出加上低秩自适应)的大小。这平衡了预训练模型的知识和新的特定于任务的适应——默认情况下,alpha通常设置为 1。另请注意,虽然W A被初始化为小的随机权重,但WB被初始化为 0,因此训练开始时ΔW = WAxWB = 0 ,这意味着我们以原始权重开始训练。

Stable Diffusion 添加lora微调代码

Part1 添加lora.py文件 - 用于设置lora层以及替换

1. 引入相关库函数

# 该模块主要是实现lora类,实现lora层的alpha和beta通路,把输入的x经过两条通路后的结果,进行联合输出。
# 然后添加一个函数,主要是为了实现将原本的线性层换曾lora层。

'''
# Part1 引入相关的库函数
'''
import torch
from torch import nn
from config import *

2. 定义LoraLayer的类

'''
# Part2 设计一个类,实现lora_layer
'''


class LoraLayer(nn.Module):
    def __init__(self, target_linear_layer, feature_in, feature_out, r, alpha):
        super().__init__()
        # 第一步,初始化lora的一些参数,包含a矩阵,b矩阵,r秩.比例系数等等。
        self.lora_a = nn.Parameter(torch.empty(feature_in, r), requires_grad=True)
        self.lora_b = nn.Parameter(torch.zeros(r, feature_out), requires_grad=True)
        self.alpha = alpha
        self.r = r

        # 第二步对alpha进行初始化
        nn.init.kaiming_uniform_(self.lora_a)

        # 第三步,初始化原本的目标线性层
        self.net = target_linear_layer

    def forward(self, x):
        output1 = self.net(x)
        output2 = torch.matmul(x, torch.matmul(self.lora_a, self.lora_b)) * (self.alpha / self.r)  # 得到结果后,乘上比例系数(alpha/r)
        return output2 + output1

3. lora层的替换

'''
# Part3 定义一个函数,实现lora层的替换
'''


def inject_lora(module, name, target_linear_layer):  # 输入完整的模型,目标线性层的位置,目标线性层
    name_list = name.split('.')  # 按照.进行拆分路径
    # 获取到目标线性层的模型的上一层所有参数和模型{模型name1:模型,模型name2:模型}
    for i, item in enumerate(name_list[:-1]):
        module = getattr(module, item)
    # 初始化需要替换进入的lora层
    lora_layer = LoraLayer(target_linear_layer,
                           feature_in=target_linear_layer.in_features, feature_out=target_linear_layer.out_features,
                           r=LORA_R, alpha=LORA_ALPHA)
    # 替换对应的层
    setattr(module, name_list[-1], lora_layer)

Part2 添加lora_finetune.py,用于参数微调训练得到lora参数.pt文件

1. 引入相关库函数

# 该模块主要实现对于模型的一些模块进行微调训练,只对lora里面的新增参数进行训练。
'''
# Part 1 引入相关的库函数
'''
import os

import torch
from torch import nn
from dataset import minist_train
from torch.utils import data
from diffusion import forward_diffusion
from config import *
from unet import Unet
from lora import inject_lora

2. 替换模型中的注意力机制里面的Wq, Wk, Wv,替换线性层

if __name__ =='__main__':
    '''
    # Part2 对需要训练的模型参数进行设置,将需要替换的线性层进行lora替换,并且只对lora进行训练
    '''
    # 首先第一步得先下载网络
    net = torch.load('unet_epoch0.pt')

    # 开始对所需的部分进行替换。
    # 首先,我们要对线性层进行lora替换,所以需要,输入inject_lora的参数包含(整个模型,路径,layer)
    for name, layer in net.named_modules():
        name_list = name.split('.')
        target = ['Wq', 'Wk', 'Wv']
        for i in target:
            if i in name_list and isinstance(layer, nn.Linear):
                # 替换
                inject_lora(net, name, layer)

    # 替换完之后,先看看需不需要添加之前的参数
    try:
        # 先下载参数
        lora_para=torch.load('lora_para_epoch0.pt')
        # 再填充到模型里面
        net.load_state_dict(lora_para,strict=False)

    except:
        pass


    # 替换完之后,需要对所有的参数进行设置,不是lora的参数梯度设置为False
    for name, para in net.named_parameters():
        name_list = name.split('.')
        lora_para_list = ['lora_a', 'lora_b']
        if name_list[-1] in lora_para_list:
            para.requires_grad = False
        else:
            para.requires_grad = True

    '''
    # Part3 进行训练
    '''
    epoch = 5
    batch_size = 50
    minist_loader = data.DataLoader(dataset=minist_train, batch_size=batch_size, shuffle=True)

    # 初始化模型
    loss = nn.L1Loss()
    opt = torch.optim.Adam(net.parameters(), lr=1e-3)

    n_iter = 0
    net.train()

    for i in range(epoch):
        for imgs, labels in minist_loader:
            imgs = imgs * 2 - 1
            # 先随机初始化batch_t
            batch_t = torch.randint(0, T, size=(imgs.size()[0],))
            # 首先对清晰图像进行加噪,得到batch_x_t
            batch_x_t, batch_noise = forward_diffusion(imgs, batch_t)

            # 预测对应的噪声
            batch_noise_pre = net(batch_x_t, batch_t, labels)

            # 计算损失
            l = loss(batch_noise, batch_noise_pre)

            # 清除梯度
            opt.zero_grad()
            # 损失反向传播
            l.backward()

            # 更新参数
            opt.step()

            # 累加损失
            last_loss = l.item()
            # 更新迭代次数
            n_iter += 1

            print('当前的iter为{},当前损失为{}'.format(n_iter, last_loss))
        print('当前的epoch为{},当前的损失为{}'.format(i, last_loss))

        # 保存训练好的lora参数,但是得先找到
        lora_dic = {}
        # 遍历net的参数
        for name, para in net.named_parameters():
            name_list = name.split('.')
            need_find = ['lora_a', 'lora_b']
            # 如果最后一个名字在需要找的参数里面
            if name_list[-1] in need_find:
                # 在存储的字典里面添加参数和名字
                lora_dic[name] = para
        # 先存储为临时文件
        torch.save(lora_dic, 'lora_para_epoch{}.pt.tmp'.format(i))
        # 然后改变路径,形成最终的参数(主要是为了防止写入出错)
        os.replace('lora_para_epoch{}.pt.tmp'.format(i), 'lora_para_epoch{}.pt'.format(i))

Part3 修改 denoise.py,修改测试的时候的lora参数加入

1. 引入相关库函数

# 该模块主要实现的是对图像进行去噪的测试。
'''
# 首先第一步,引入相关的库函数
'''

import torch
from torch import nn
from config import *
from diffusion import alpha_t, alpha_bar
from dataset import *
import matplotlib.pyplot as plt
from diffusion import forward_diffusion
from lora import inject_lora
from lora import LoraLayer

2. 定义去噪的函数

'''
# 第二步定义一个去噪的函数
'''


def backward_denoise(net, batch_x_t, batch_labels):
    # 首先计算所需要的数据,方差variance,也就公式里面的beta_t
    alpha_bar_late = torch.cat((torch.tensor([1.0]), alpha_bar[:-1]), dim=0)
    variance = (1 - alpha_t) * (1 - alpha_bar_late) / (1 - alpha_bar)
    # 得到方差后,开始去噪
    net.eval()  # 开启测试模式
    # 记录每次得到的图像
    steps = [batch_x_t]
    for t in range(T - 1, -1, -1):
        # 初始化当前每张图像对应的时间状态
        batch_t = torch.full(size=(batch_x_t.size()[0],), fill_value=t)  # 表示此时的时间状态 (batch,)
        # 预测噪声
        # 修改第十四处
        batch_noise_pre = net(batch_x_t, batch_t, batch_labels)  # (batch,channel,iamg,imag)

        # 开始去噪(需要注意一个点,就是去噪的公式,在t不等于0和等于0是不一样的,先进行都需要处理部分也就是添加噪声前面的均值部分)
        # 同时记得要统一维度,便于广播
        reshape_size = (batch_t.size()[0], 1, 1, 1)
        # 先取出对应的数值
        alpha_t_batch = alpha_t[batch_t]
        alpha_bar_batch = alpha_bar[batch_t]
        variance_batch = variance[batch_t]
        # 计算前面的均值
        batch_mean_t = 1 / torch.sqrt(alpha_t_batch).reshape(*reshape_size) \
                       * (batch_x_t - (1 - alpha_t_batch.reshape(*reshape_size)) * batch_noise_pre / torch.sqrt(
            1 - alpha_bar_batch.reshape(*reshape_size)))

        # 分类,看t的值,判断是否添加噪声
        if t != 0:
            batch_x_t = batch_mean_t \
                        + torch.sqrt(variance_batch.reshape(*reshape_size)) \
                        * torch.randn_like(batch_x_t)
        else:
            batch_x_t = batch_mean_t

        # 对每次得到的结果进行上下限的限制
        batch_x_t = torch.clamp(batch_x_t, min=-1, max=1)
        # 添加每步的去噪结果
        steps.append(batch_x_t)
    return steps

3. 测试-去噪

# 开始测试
if __name__ == '__main__':
    # 加载模型
    model = torch.load('unet_epoch0.pt')
    model.eval()

    is_lora = True
    is_hebing = False
    # 如果是利用lora,需要把微调的也加进去模型进行推理
    if is_lora:
        for name, layer in model.named_modules():
            name_list = name.split('.')
            target_list = ['Wk', 'Wv', 'Wq']
            for i in target_list:
                if i in name_list and isinstance(layer, nn.Linear):
                    inject_lora(model, name, layer)

        # 加载权重参数
        try:
            para_load = torch.load('lora_para_epoch0.pt')
            model.load_state_dict(para_load, strict=False)
        except:
            pass

    # 如果需要合并,也就是把lora参数添加到原本的线性层上面的话,也就是把插入重新实现一遍,这次是把lora_layer换成linear。
    if is_lora and is_hebing:
        for name, layer in model:
            name_list = name.split('.')

            if isinstance(layer, LoraLayer):
                # 找到了对应的参数,把对应的lora参数添加到原本的参数上
                # 为什么要确定参数位置的上一层,因为setattr只能在上一层用,不能层层进入属性。
                cur_layer=model
                for n in name_list[:-1]:
                    cur_layer=getattr(cur_layer,n)
                # 首先计算lora参数
                lora_weight = torch.matmul(layer.lora_a, layer.lora_b) * layer.alpha / layer.r

                # 把参数进行添加,线性层的权重矩阵通常是 (out_features, in_features),所以需要对lora矩阵进行转置
                layer.net.weight = nn.Parameter(layer.net.weight.add(lora_weight.T))
                setattr(cur_layer, name_list[-1], layer)

    # 生成噪音图
    batch_size = 10
    batch_x_t = torch.randn(size=(batch_size, 1, IMAGE_SIZE, IMAGE_SIZE))  # (5,1,48,48)
    batch_labels = torch.arange(start=0, end=10, dtype=torch.long)  # 引导词promot
    # 逐步去噪得到原图
    # 修改第十五处
    steps = backward_denoise(model, batch_x_t, batch_labels)
    # 绘制数量
    num_imgs = 20
    # 绘制还原过程
    plt.figure(figsize=(15, 15))
    for b in range(batch_size):
        for i in range(0, num_imgs):
            idx = int(T / num_imgs) * (i + 1)
            # 像素值还原到[0,1]
            final_img = (steps[idx][b] + 1) / 2
            # tensor转回PIL图
            final_img = TenosrtoPil_action(final_img)
            plt.subplot(batch_size, num_imgs, b * num_imgs + i + 1)
            plt.imshow(final_img)
    plt.show()

参考

视频讲解:Lora微调代码实现_哔哩哔哩_bilibili

原理博客:手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion(原理介绍)-CSDN博客,自学资料 - LoRA - 低秩微调技术-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/961817.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于 AWS SageMaker 对 DeepSeek-R1-Distilled-Llama-8B 模型的精调与实践

在当今人工智能蓬勃发展的时代,语言模型的性能优化和定制化成为研究与应用的关键方向。本文聚焦于 AWS SageMaker 平台上对 DeepSeek-R1-Distilled-Llama-8B 模型的精调实践,详细探讨这一过程中的技术细节、操作步骤以及实践价值。 一、实验背景与目标 …

三、SysTick系统节拍定时器

3.1 SysTick简介 系统节拍定时器SysTick是ARM Cortex-M0内核提供的一个24位递减定时器,当计数值达到0时产生中断,可以为操作系统和其他管理软件提供固定时间的中断。 当系统节拍定时器被被使能时,定时器从重装值递减计数,到0进中断…

算法每日双题精讲 —— 前缀和(【模板】一维前缀和,【模板】二维前缀和)

在算法竞赛与日常编程中,前缀和是一种极为实用的预处理技巧,能显著提升处理区间和问题的效率。今天,我们就来深入剖析一维前缀和与二维前缀和这两个经典模板。 一、【模板】一维前缀和 题目描述 给定一个长度为 n n n 的整数数组 a a a&…

学习数据结构(2)空间复杂度+顺序表

1.空间复杂度 (1)概念 空间复杂度也是一个数学表达式,表示一个算法在运行过程中根据算法的需要额外临时开辟的空间。 空间复杂度不是指程序占用了多少bytes的空间,因为常规情况每个对象大小差异不会很大,所以空间复杂…

MybatisX插件快速创建项目

一、安装插件 二、创建一个数据表测试 三、IDEA连接Mysql数据库 四、选择MybatiX构造器 五、配置参数 六、项目结构

基于SpringBoot的假期周边游平台的设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…

Java设计模式:结构型模式→组合模式

Java 组合模式详解 1. 定义 组合模式(Composite Pattern)是一种结构型设计模式,它允许将对象组合成树形结构以表示“部分-整体”的层次。组合模式使得客户端能够以统一的方式对待单个对象和对象集合的一致性,有助于处理树形结构…

FastReport.NET控件篇之富文本控件

简介 FastReport.NET 提供了 RichText 控件,用于在报表中显示富文本内容。富文本控件支持多种文本格式(如字体、颜色、段落、表格、图片等),非常适合需要复杂排版和格式化的场景。 富文本控件(RichText)使用场景不多&#xff0c…

单片机基础模块学习——NE555芯片

一、NE555电路图 NE555也称555定时器,本文主要利用NE555产生方波发生电路。整个电路相当于频率可调的方波发生器。 通过调整电位器的阻值,方波的频率也随之改变。 RB3在开发板的位置如下图 测量方波信号的引脚为SIGHAL,由上面的电路图可知,NE555已经构成完整的方波发生电…

(done) MIT6.S081 2023 学习笔记 (Day6: LAB5 COW Fork)

网页:https://pdos.csail.mit.edu/6.S081/2023/labs/cow.html 任务1:Implement copy-on-write fork(hard) (完成) 现实中的问题如下: xv6中的fork()系统调用会将父进程的用户空间内存全部复制到子进程中。如果父进程很大,复制过程…

三天急速通关JavaWeb基础知识:Day 1 后端基础知识

三天急速通关JavaWeb基础知识:Day 1 后端基础知识 0 文章说明1 Http1.1 介绍1.2 通信过程1.3 报文 Message1.3.1 请求报文 Request Message1.3.2 响应报文 Response Message 2 XML2.1 介绍2.2 利用Java解析XML 3 Tomcat3.1 介绍3.2 Tomcat的安装与配置3.3 Tomcat的项…

SQLServer 不允许保存更改(主键)

在我们进行数据库表格编辑的时候,往往会出现同一个名字,就比如我们的账号一样,我们在注册自己QQ的时候,我们通常注册过的账号,别人就不能注册了,这是为了保证严密性 所以我们需要点击表格>右键>设计 点击某一列>右键>设计主键 当我们Ctrls 保存的时候回弹出下…

【hot100】刷题记录(6)-轮转数组

题目描述: 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转…

(非技术)从一公里到半程马拉松:我的一年跑步经历

在24年初,从来不运动的我,连跑步一公里都不能完成。而在一年之后的2025年的1月1日,我参加了上海的蒸蒸日上迎新跑,完成了半程马拉松。虽然速度不快,也并不是什么特别难完成的事情,但对我来说还是挺有意义的…

知识蒸馏技术原理详解:从软标签到模型压缩的实现机制

知识蒸馏是一种通过性能与模型规模的权衡来实现模型压缩的技术。其核心思想是将较大规模模型(称为教师模型)中的知识迁移到规模较小的模型(称为学生模型)中。本文将深入探讨知识迁移的具体实现机制。 知识蒸馏原理 知识蒸馏的核心…

Linux——冯 • 诺依曼体系结构

目录 一、冯•诺依曼体系结构原理二、内存提高冯•诺依曼体系结构效率的方法三、当用QQ和朋友聊天时数据的流动过程四、关于冯诺依曼五、总结 我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系 流程&#…

JavaScript - Web APIs(下)

日期对象 目标:掌握日期对象,可以让网页显示日期 日期对象:用来表示时间的对象 作用:可以得到当前系统时间 学习路径: 实例化 日期对象方法 时间戳 实例化 目标:能够实例化日期对象 在代码中发…

【make】makefile变量全解

目录 makefile简介变量全解变量基础变量高级使用1. 将变量里的值进行替换后输出2. 使用变量的嵌套使用3. $ 可以组合使用 override 指示符目标指定变量模式变量 总结参考链接 makefile简介 makefile 是一种类似shell的脚本文件,需要make工具进行解释 makefile 内的语…

51单片机入门_02_C语言基础0102

C语言基础部分可以参考我之前写的专栏C语言基础入门48篇 以及《从入门到就业C全栈班》中的C语言部分,本篇将会结合51单片机讲差异部分。 课程主要按照以下目录进行介绍。 文章目录 1. 进制转换2. C语言简介3. C语言中基本数据类型4. 标识符与关键字5. 变量与常量6.…

常见的同态加密算法收集

随着对crypten与密码学的了解,我们将逐渐深入学习相关知识。今天,我们将跟随同态加密的发展历程对相关算法进行简单的收集整理 。 目录 同态加密概念 RSA算法 ElGamal算法 ELGamal签名算法 Paillier算法 BGN方案 Gentry 方案 BGV 方案 BFV 方案…